[{"data":1,"prerenderedAt":12228},["ShallowReactive",2],{"/writing":3,"articles":36},{"id":4,"title":5,"body":6,"date":27,"description":23,"extension":28,"head":27,"meta":29,"navigation":30,"ogImage":27,"path":31,"robots":27,"schemaOrg":27,"seo":32,"sitemap":33,"stem":34,"__hash__":35},"content/3.writing.md","Writing",{"type":7,"value":8,"toc":24},"minimark",[9],[10,11,12,19],"writing",{},[13,14,16],"template",{"v-slot:title":15},"",[17,18,5],"p",{},[13,20,21],{"v-slot:subtitle":15},[17,22,23],{},"Some writings on topics that are close to my heart",{"title":15,"searchDepth":25,"depth":25,"links":26},2,[],null,"md",{},true,"/writing",{"title":5,"description":23},{"loc":31},"3.writing","cvvaLdRuzzkXMLS4zogeDAa0Z2Y_Kyt22cjDTJq7AoQ",[37,10060,11111,12097,12185],{"id":38,"title":39,"body":40,"date":10047,"description":10048,"extension":28,"head":27,"image":48,"meta":10049,"navigation":30,"ogImage":27,"path":10050,"readingTime":10051,"robots":27,"schemaOrg":27,"seo":10052,"sitemap":10053,"stem":10054,"tags":10055,"__hash__":10059},"articles/articles/00.the-electron-experience.md","Building Desktop Apps on The Solana Blockchain with Electron",{"type":7,"value":41,"toc":10032},[42,49,66,71,86,100,120,129,138,147,151,166,388,403,408,411,424,435,1349,1366,1369,1377,1380,1395,1537,1548,2205,2208,2222,2225,2254,2257,2261,2264,2275,2278,2281,2430,2437,2443,2452,2455,2458,2830,2837,2843,3176,3179,3192,3199,3202,3444,3447,3454,3603,3614,3827,3836,3839,3849,3852,3859,4074,4085,4088,4095,4486,4492,4503,4506,4517,4688,4694,5483,5492,5930,5936,5949,5952,5956,5959,5962,5983,5996,6016,6040,6049,6052,6065,6068,6074,6077,6901,6912,6923,6950,6955,6961,6966,7237,7240,7244,7247,7250,7253,7260,7596,7606,8159,8163,8166,8169,8178,8181,8185,8196,8199,8535,8538,8548,8640,8654,8658,8661,8664,8667,8815,8825,8836,8852,8972,8992,8996,9002,9011,9021,9031,9907,9918,10009,10012,10015,10028],[17,43,44],{},[45,46],"img",{"alt":47,"src":48},"preview","/articles/the-solana-experince.png",[17,50,51,52,59,60,65],{},"Around 3 weeks ago at the time of writing this post, I was asked by a friend \"Can you build me a trading bot that can trade meme coins?\", I then thought to myself \"well why not, how hard can that possibly be?, if I keep it simple and avoid over engineering this\", so I asked what capabilities you want your bot to have, he said \"just the basics, buy/sell and if you can add a volume bot to it also\". Well, on the surface especially when you're not familiar with blockchain tech and its ecosystem that sounds easy, and a great learning experience, so I agreed. Guess what? I was brutally wrong. And only realized that when I started researching this whole topic. You can find the final App in its ",[53,54,58],"a",{"href":55,"rel":56},"https://solana.riavzon.com/",[57],"nofollow","website"," or ",[53,61,64],{"href":62,"rel":63},"https://github.com/Sergo706/solana-bots",[57],"Github",".",[67,68,70],"h2",{"id":69},"researching","Researching",[17,72,73,74,79,80,85],{},"I started my research on what meme coins even are, how they are created, and how one can buy or sell these. What I found were mostly existing trading platforms, that all point to creating/importing wallets, adding SOL to them and start trading. So I searched \"what sols are\", and discovered ",[53,75,78],{"href":76,"rel":77},"https://solana.com/",[57],"Solana",", and that sols are a cryptocurrency, and Solana is the primary blockchain of these, and that ",[53,81,84],{"href":82,"rel":83},"https://solana.com/learn/what-is-a-wallet",[57],"wallets"," are the thing you keep these sols in and trade with, and they consist of key pairs of public key (the address you use to receive funds) and a private key (the secret key you use to sign transactions and send funds). So I figured out that my bots, are actually wallets, and the thing that powers them is SOL.",[17,87,88,89,94,95,65],{},"After exploring the platform's docs, and examples, I started experimenting creating wallets, adding SOL to them with Airdrops via the public rpc ",[53,90,93],{"href":91,"rel":92},"https://solana.com/docs/rpc",[57],"endpoint"," (specifically the devnet endpoint), and sending them around between wallets with ",[53,96,99],{"href":97,"rel":98},"https://solana.com/docs/core/transactions",[57],"transactions",[17,101,102,103,108,109,114,115,65],{},"And that \"meme coins\" are ",[53,104,107],{"href":105,"rel":106},"https://solana.com/docs/tokens/basics/create-mint",[57],"Token mints"," with ",[53,110,113],{"href":111,"rel":112},"https://solana.com/docs/tokens/metaplex",[57],"metaplex metadata",". You use your wallets to sell and receive tokens, from these mints, with a DEX such as ",[53,116,119],{"href":117,"rel":118},"https://raydium.io/",[57],"Raydium",[17,121,122,123,128],{},"At this point I got almost all of that already figured out, what I realized more about this ecosystem, is the scam potential it has over people that have absolutely no idea, how easy it is, to pull off ",[53,124,127],{"href":125,"rel":126},"https://en.wikipedia.org/wiki/Exit_scam",[57],"rug pulls",", manipulate pools and control your coins as a developer. Which most of this ecosystem is built around this idea \"it's just part of the game\", and completely accept that. What I found out more, is that there are huge enterprises who know all that and build their entire business model around these scams at scale.",[17,130,131,132,137],{},"With the research done, I started creating the dev environment, and with the idea of keeping it simple but usable and easy enough for my friend to use, I picked ",[53,133,136],{"href":134,"rel":135},"https://developers.jup.ag/",[57],"Jupiter"," as the DEX aggregator to easily get the best prices and quotes, and avoid complex math and logic, they have generous rate limits (at the time of writing) 1/rps and unlimited usage, which is more than enough for my friend. I told him \"it will be a cli without any UI at first, see how you get around that, and if you find it challenging, I'll create you a desktop app\".",[17,139,140,141,146],{},"Why not? I have a good knowledge of Vue, React, and Electron shouldn't be that hard to navigate, as it's just web technologies, and their build tool, ",[53,142,145],{"href":143,"rel":144},"https://www.electronforge.io/",[57],"Electron forge"," can be used with Vite, React, Vue and TypeScript, plus their docs state \"It combines many single-purpose packages to create a full build pipeline that works out of the box\" so it should be easy right?...",[67,148,150],{"id":149},"development","Development",[17,152,153,154,159,160,165],{},"Starting with the CLI, I set up my ",[53,155,158],{"href":156,"rel":157},"https://github.com/Sergo706/utils/blob/main/eslint/strict.ts.config.ts",[57],"eslint config"," installed ",[53,161,164],{"href":162,"rel":163},"https://github.com/unjs/citty",[57],"citty"," as the CLI builder, drizzle with better-sqlite3 for wallets storage, and started the development. I defined the schema to be:",[167,168,173],"pre",{"className":169,"code":170,"filename":171,"language":172,"meta":15,"style":15},"language-ts shiki shiki-themes github-light github-dark github-dark","import { blob, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { sql } from \"drizzle-orm\";\n\nexport const walletTable = sqliteTable('wallets', {\n    id: integer('id', {mode: 'number'}).primaryKey({ autoIncrement: true }),\n    publicAddress: text('public_address').unique(),\n    isMainWallet: integer('is_main_wallet', {mode: 'boolean'}),\n    privateKeyEncrypted: blob('private_key_encrypted').notNull(),\n    createdAt: integer('created_at').notNull().default(sql`(cast(unixepoch() as int))`),\n});\n","wallets.ts","ts",[174,175,176,199,213,219,248,283,306,327,348,382],"code",{"__ignoreMap":15},[177,178,181,185,189,192,196],"span",{"class":179,"line":180},"line",1,[177,182,184],{"class":183},"so5gQ","import",[177,186,188],{"class":187},"slsVL"," { blob, integer, sqliteTable, text } ",[177,190,191],{"class":183},"from",[177,193,195],{"class":194},"sfrk1"," \"drizzle-orm/sqlite-core\"",[177,197,198],{"class":187},";\n",[177,200,201,203,206,208,211],{"class":179,"line":25},[177,202,184],{"class":183},[177,204,205],{"class":187}," { sql } ",[177,207,191],{"class":183},[177,209,210],{"class":194}," \"drizzle-orm\"",[177,212,198],{"class":187},[177,214,216],{"class":179,"line":215},3,[177,217,218],{"emptyLinePlaceholder":30},"\n",[177,220,222,225,228,232,235,239,242,245],{"class":179,"line":221},4,[177,223,224],{"class":183},"export",[177,226,227],{"class":183}," const",[177,229,231],{"class":230},"suiK_"," walletTable",[177,233,234],{"class":183}," =",[177,236,238],{"class":237},"shcOC"," sqliteTable",[177,240,241],{"class":187},"(",[177,243,244],{"class":194},"'wallets'",[177,246,247],{"class":187},", {\n",[177,249,251,254,257,259,262,265,268,271,274,277,280],{"class":179,"line":250},5,[177,252,253],{"class":187},"    id: ",[177,255,256],{"class":237},"integer",[177,258,241],{"class":187},[177,260,261],{"class":194},"'id'",[177,263,264],{"class":187},", {mode: ",[177,266,267],{"class":194},"'number'",[177,269,270],{"class":187},"}).",[177,272,273],{"class":237},"primaryKey",[177,275,276],{"class":187},"({ autoIncrement: ",[177,278,279],{"class":230},"true",[177,281,282],{"class":187}," }),\n",[177,284,286,289,292,294,297,300,303],{"class":179,"line":285},6,[177,287,288],{"class":187},"    publicAddress: ",[177,290,291],{"class":237},"text",[177,293,241],{"class":187},[177,295,296],{"class":194},"'public_address'",[177,298,299],{"class":187},").",[177,301,302],{"class":237},"unique",[177,304,305],{"class":187},"(),\n",[177,307,309,312,314,316,319,321,324],{"class":179,"line":308},7,[177,310,311],{"class":187},"    isMainWallet: ",[177,313,256],{"class":237},[177,315,241],{"class":187},[177,317,318],{"class":194},"'is_main_wallet'",[177,320,264],{"class":187},[177,322,323],{"class":194},"'boolean'",[177,325,326],{"class":187},"}),\n",[177,328,330,333,336,338,341,343,346],{"class":179,"line":329},8,[177,331,332],{"class":187},"    privateKeyEncrypted: ",[177,334,335],{"class":237},"blob",[177,337,241],{"class":187},[177,339,340],{"class":194},"'private_key_encrypted'",[177,342,299],{"class":187},[177,344,345],{"class":237},"notNull",[177,347,305],{"class":187},[177,349,351,354,356,358,361,363,365,368,371,373,376,379],{"class":179,"line":350},9,[177,352,353],{"class":187},"    createdAt: ",[177,355,256],{"class":237},[177,357,241],{"class":187},[177,359,360],{"class":194},"'created_at'",[177,362,299],{"class":187},[177,364,345],{"class":237},[177,366,367],{"class":187},"().",[177,369,370],{"class":237},"default",[177,372,241],{"class":187},[177,374,375],{"class":237},"sql",[177,377,378],{"class":194},"`(cast(unixepoch() as int))`",[177,380,381],{"class":187},"),\n",[177,383,385],{"class":179,"line":384},10,[177,386,387],{"class":187},"});\n",[17,389,390,391,394,395,399,400,402],{},"Nothing crazy, except that the ",[174,392,393],{},"isMainWallet"," column filters a wallet as a main one, only a single wallet exists in the system that is flagged as main. This wallet is directly used to distribute funds to ",[396,397,398],"em",{},"sub-wallets"," which their ",[174,401,393],{}," is 0. And to drain sub-wallets back to main.",[404,405,407],"h3",{"id":406},"creating-wallets","Creating Wallets",[17,409,410],{},"Obviously, the private key of a wallet is a sensitive field, and should be kept secret, I needed a way to keep that field:",[412,413,414,418,421],"ul",{},[415,416,417],"li",{},"Encrypted when a new wallet is stored",[415,419,420],{},"Decrypted on demand when a sensitive action is requested",[415,422,423],{},"Needs to be simple enough for a non-technical user to use and navigate",[17,425,426,427,430,431,434],{},"So I thought, why not use ",[174,428,429],{},"aes-256-gcm","? It gives you data integrity (eg. authentication) and I can ask the user for a \"password\", which is hashed and salted with ",[174,432,433],{},"scrypt"," to derive a cryptographic key. This derived key is then fed directly into the AES algorithm to encrypt the wallet's private key. Then, for the decryption function I can ask for that same password with the encrypted value. So that's what I ended up with:",[167,436,439],{"className":169,"code":437,"filename":438,"language":172,"meta":15,"style":15},"const SCRYPT_CONFIG = {\n  keyLen: 32,\n  saltLen: 32,\n  params: { N: 16384, r: 8, p: 1 }\n};\n\n\nexport const encryptPrivateKey = (privateKey: Uint8Array, password: string): Buffer => {\n  try {\n    const iv = crypto.randomBytes(12);\n    const salt = crypto.randomBytes(SCRYPT_CONFIG.saltLen);\n    const key = crypto.scryptSync(password, salt, SCRYPT_CONFIG.keyLen, SCRYPT_CONFIG.params);\n    \n    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);\n    const encrypted = Buffer.concat([cipher.update(privateKey), cipher.final()]);\n    const tag = cipher.getAuthTag();\n\n    return Buffer.concat([salt, iv, tag, encrypted]);\n  } catch (err) {\n    throw new Error(`Encryption failed: ${(err as Error).message}`);\n  }\n};\n\nexport const decryptPrivateKey = (encrypted: Buffer, password: string): Uint8Array => {\n  try {\n\n    const saltLen = SCRYPT_CONFIG.saltLen;\n    const ivLen = 12; \n    const tagLen = 16;\n    const minLength = saltLen + ivLen + tagLen;\n\n    if (encrypted.length \u003C minLength) {\n      throw new Error('Invalid encrypted data format');\n    }\n\n    let offset = 0;\n    const salt = encrypted.subarray(offset, offset += saltLen);\n    const iv = encrypted.subarray(offset, offset += ivLen);\n    const tag = encrypted.subarray(offset, offset += tagLen);\n    const ciphertext = encrypted.subarray(offset);\n\n    const key = crypto.scryptSync(password, salt, SCRYPT_CONFIG.keyLen, SCRYPT_CONFIG.params);\n    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);\n    decipher.setAuthTag(tag);\n\n    return new Uint8Array(Buffer.concat([\n      decipher.update(ciphertext),\n      decipher.final()\n    ]));\n  } catch (error: unknown) {\n    const err = error instanceof Error ? error : null;\n    const code = (error as NodeJS.ErrnoException).code;\n    if (code === 'ERR_CRYPTO_OPERATION_FAILED' || err?.message.toLowerCase().includes('unsupported state')) {\n      throw new Error('Incorrect password or data');\n    }\n    consola.log(`Decryption failure`);\n    throw error;\n  }\n};\n","utils.ts",[174,440,441,454,465,474,497,502,506,510,558,565,589,611,639,645,668,699,718,723,736,748,785,791,796,801,840,847,852,867,883,898,922,927,945,962,968,973,990,1014,1034,1054,1071,1076,1099,1120,1132,1137,1154,1165,1175,1181,1201,1231,1258,1294,1310,1315,1331,1339,1344],{"__ignoreMap":15},[177,442,443,446,449,451],{"class":179,"line":180},[177,444,445],{"class":183},"const",[177,447,448],{"class":230}," SCRYPT_CONFIG",[177,450,234],{"class":183},[177,452,453],{"class":187}," {\n",[177,455,456,459,462],{"class":179,"line":25},[177,457,458],{"class":187},"  keyLen: ",[177,460,461],{"class":230},"32",[177,463,464],{"class":187},",\n",[177,466,467,470,472],{"class":179,"line":215},[177,468,469],{"class":187},"  saltLen: ",[177,471,461],{"class":230},[177,473,464],{"class":187},[177,475,476,479,482,485,488,491,494],{"class":179,"line":221},[177,477,478],{"class":187},"  params: { N: ",[177,480,481],{"class":230},"16384",[177,483,484],{"class":187},", r: ",[177,486,487],{"class":230},"8",[177,489,490],{"class":187},", p: ",[177,492,493],{"class":230},"1",[177,495,496],{"class":187}," }\n",[177,498,499],{"class":179,"line":250},[177,500,501],{"class":187},"};\n",[177,503,504],{"class":179,"line":285},[177,505,218],{"emptyLinePlaceholder":30},[177,507,508],{"class":179,"line":308},[177,509,218],{"emptyLinePlaceholder":30},[177,511,512,514,516,519,521,524,528,531,534,537,540,542,545,548,550,553,556],{"class":179,"line":329},[177,513,224],{"class":183},[177,515,227],{"class":183},[177,517,518],{"class":237}," encryptPrivateKey",[177,520,234],{"class":183},[177,522,523],{"class":187}," (",[177,525,527],{"class":526},"sQHwn","privateKey",[177,529,530],{"class":183},":",[177,532,533],{"class":237}," Uint8Array",[177,535,536],{"class":187},", ",[177,538,539],{"class":526},"password",[177,541,530],{"class":183},[177,543,544],{"class":230}," string",[177,546,547],{"class":187},")",[177,549,530],{"class":183},[177,551,552],{"class":237}," Buffer",[177,554,555],{"class":183}," =>",[177,557,453],{"class":187},[177,559,560,563],{"class":179,"line":350},[177,561,562],{"class":183},"  try",[177,564,453],{"class":187},[177,566,567,570,573,575,578,581,583,586],{"class":179,"line":384},[177,568,569],{"class":183},"    const",[177,571,572],{"class":230}," iv",[177,574,234],{"class":183},[177,576,577],{"class":187}," crypto.",[177,579,580],{"class":237},"randomBytes",[177,582,241],{"class":187},[177,584,585],{"class":230},"12",[177,587,588],{"class":187},");\n",[177,590,592,594,597,599,601,603,605,608],{"class":179,"line":591},11,[177,593,569],{"class":183},[177,595,596],{"class":230}," salt",[177,598,234],{"class":183},[177,600,577],{"class":187},[177,602,580],{"class":237},[177,604,241],{"class":187},[177,606,607],{"class":230},"SCRYPT_CONFIG",[177,609,610],{"class":187},".saltLen);\n",[177,612,614,616,619,621,623,626,629,631,634,636],{"class":179,"line":613},12,[177,615,569],{"class":183},[177,617,618],{"class":230}," key",[177,620,234],{"class":183},[177,622,577],{"class":187},[177,624,625],{"class":237},"scryptSync",[177,627,628],{"class":187},"(password, salt, ",[177,630,607],{"class":230},[177,632,633],{"class":187},".keyLen, ",[177,635,607],{"class":230},[177,637,638],{"class":187},".params);\n",[177,640,642],{"class":179,"line":641},13,[177,643,644],{"class":187},"    \n",[177,646,648,650,653,655,657,660,662,665],{"class":179,"line":647},14,[177,649,569],{"class":183},[177,651,652],{"class":230}," cipher",[177,654,234],{"class":183},[177,656,577],{"class":187},[177,658,659],{"class":237},"createCipheriv",[177,661,241],{"class":187},[177,663,664],{"class":194},"'aes-256-gcm'",[177,666,667],{"class":187},", key, iv);\n",[177,669,671,673,676,678,681,684,687,690,693,696],{"class":179,"line":670},15,[177,672,569],{"class":183},[177,674,675],{"class":230}," encrypted",[177,677,234],{"class":183},[177,679,680],{"class":187}," Buffer.",[177,682,683],{"class":237},"concat",[177,685,686],{"class":187},"([cipher.",[177,688,689],{"class":237},"update",[177,691,692],{"class":187},"(privateKey), cipher.",[177,694,695],{"class":237},"final",[177,697,698],{"class":187},"()]);\n",[177,700,702,704,707,709,712,715],{"class":179,"line":701},16,[177,703,569],{"class":183},[177,705,706],{"class":230}," tag",[177,708,234],{"class":183},[177,710,711],{"class":187}," cipher.",[177,713,714],{"class":237},"getAuthTag",[177,716,717],{"class":187},"();\n",[177,719,721],{"class":179,"line":720},17,[177,722,218],{"emptyLinePlaceholder":30},[177,724,726,729,731,733],{"class":179,"line":725},18,[177,727,728],{"class":183},"    return",[177,730,680],{"class":187},[177,732,683],{"class":237},[177,734,735],{"class":187},"([salt, iv, tag, encrypted]);\n",[177,737,739,742,745],{"class":179,"line":738},19,[177,740,741],{"class":187},"  } ",[177,743,744],{"class":183},"catch",[177,746,747],{"class":187}," (err) {\n",[177,749,751,754,757,760,762,765,767,770,773,775,777,780,783],{"class":179,"line":750},20,[177,752,753],{"class":183},"    throw",[177,755,756],{"class":183}," new",[177,758,759],{"class":237}," Error",[177,761,241],{"class":187},[177,763,764],{"class":194},"`Encryption failed: ${",[177,766,241],{"class":194},[177,768,769],{"class":187},"err",[177,771,772],{"class":183}," as",[177,774,759],{"class":237},[177,776,299],{"class":194},[177,778,779],{"class":187},"message",[177,781,782],{"class":194},"}`",[177,784,588],{"class":187},[177,786,788],{"class":179,"line":787},21,[177,789,790],{"class":187},"  }\n",[177,792,794],{"class":179,"line":793},22,[177,795,501],{"class":187},[177,797,799],{"class":179,"line":798},23,[177,800,218],{"emptyLinePlaceholder":30},[177,802,804,806,808,811,813,815,818,820,822,824,826,828,830,832,834,836,838],{"class":179,"line":803},24,[177,805,224],{"class":183},[177,807,227],{"class":183},[177,809,810],{"class":237}," decryptPrivateKey",[177,812,234],{"class":183},[177,814,523],{"class":187},[177,816,817],{"class":526},"encrypted",[177,819,530],{"class":183},[177,821,552],{"class":237},[177,823,536],{"class":187},[177,825,539],{"class":526},[177,827,530],{"class":183},[177,829,544],{"class":230},[177,831,547],{"class":187},[177,833,530],{"class":183},[177,835,533],{"class":237},[177,837,555],{"class":183},[177,839,453],{"class":187},[177,841,843,845],{"class":179,"line":842},25,[177,844,562],{"class":183},[177,846,453],{"class":187},[177,848,850],{"class":179,"line":849},26,[177,851,218],{"emptyLinePlaceholder":30},[177,853,855,857,860,862,864],{"class":179,"line":854},27,[177,856,569],{"class":183},[177,858,859],{"class":230}," saltLen",[177,861,234],{"class":183},[177,863,448],{"class":230},[177,865,866],{"class":187},".saltLen;\n",[177,868,870,872,875,877,880],{"class":179,"line":869},28,[177,871,569],{"class":183},[177,873,874],{"class":230}," ivLen",[177,876,234],{"class":183},[177,878,879],{"class":230}," 12",[177,881,882],{"class":187},"; \n",[177,884,886,888,891,893,896],{"class":179,"line":885},29,[177,887,569],{"class":183},[177,889,890],{"class":230}," tagLen",[177,892,234],{"class":183},[177,894,895],{"class":230}," 16",[177,897,198],{"class":187},[177,899,901,903,906,908,911,914,917,919],{"class":179,"line":900},30,[177,902,569],{"class":183},[177,904,905],{"class":230}," minLength",[177,907,234],{"class":183},[177,909,910],{"class":187}," saltLen ",[177,912,913],{"class":183},"+",[177,915,916],{"class":187}," ivLen ",[177,918,913],{"class":183},[177,920,921],{"class":187}," tagLen;\n",[177,923,925],{"class":179,"line":924},31,[177,926,218],{"emptyLinePlaceholder":30},[177,928,930,933,936,939,942],{"class":179,"line":929},32,[177,931,932],{"class":183},"    if",[177,934,935],{"class":187}," (encrypted.",[177,937,938],{"class":230},"length",[177,940,941],{"class":183}," \u003C",[177,943,944],{"class":187}," minLength) {\n",[177,946,948,951,953,955,957,960],{"class":179,"line":947},33,[177,949,950],{"class":183},"      throw",[177,952,756],{"class":183},[177,954,759],{"class":237},[177,956,241],{"class":187},[177,958,959],{"class":194},"'Invalid encrypted data format'",[177,961,588],{"class":187},[177,963,965],{"class":179,"line":964},34,[177,966,967],{"class":187},"    }\n",[177,969,971],{"class":179,"line":970},35,[177,972,218],{"emptyLinePlaceholder":30},[177,974,976,979,982,985,988],{"class":179,"line":975},36,[177,977,978],{"class":183},"    let",[177,980,981],{"class":187}," offset ",[177,983,984],{"class":183},"=",[177,986,987],{"class":230}," 0",[177,989,198],{"class":187},[177,991,993,995,997,999,1002,1005,1008,1011],{"class":179,"line":992},37,[177,994,569],{"class":183},[177,996,596],{"class":230},[177,998,234],{"class":183},[177,1000,1001],{"class":187}," encrypted.",[177,1003,1004],{"class":237},"subarray",[177,1006,1007],{"class":187},"(offset, offset ",[177,1009,1010],{"class":183},"+=",[177,1012,1013],{"class":187}," saltLen);\n",[177,1015,1017,1019,1021,1023,1025,1027,1029,1031],{"class":179,"line":1016},38,[177,1018,569],{"class":183},[177,1020,572],{"class":230},[177,1022,234],{"class":183},[177,1024,1001],{"class":187},[177,1026,1004],{"class":237},[177,1028,1007],{"class":187},[177,1030,1010],{"class":183},[177,1032,1033],{"class":187}," ivLen);\n",[177,1035,1037,1039,1041,1043,1045,1047,1049,1051],{"class":179,"line":1036},39,[177,1038,569],{"class":183},[177,1040,706],{"class":230},[177,1042,234],{"class":183},[177,1044,1001],{"class":187},[177,1046,1004],{"class":237},[177,1048,1007],{"class":187},[177,1050,1010],{"class":183},[177,1052,1053],{"class":187}," tagLen);\n",[177,1055,1057,1059,1062,1064,1066,1068],{"class":179,"line":1056},40,[177,1058,569],{"class":183},[177,1060,1061],{"class":230}," ciphertext",[177,1063,234],{"class":183},[177,1065,1001],{"class":187},[177,1067,1004],{"class":237},[177,1069,1070],{"class":187},"(offset);\n",[177,1072,1074],{"class":179,"line":1073},41,[177,1075,218],{"emptyLinePlaceholder":30},[177,1077,1079,1081,1083,1085,1087,1089,1091,1093,1095,1097],{"class":179,"line":1078},42,[177,1080,569],{"class":183},[177,1082,618],{"class":230},[177,1084,234],{"class":183},[177,1086,577],{"class":187},[177,1088,625],{"class":237},[177,1090,628],{"class":187},[177,1092,607],{"class":230},[177,1094,633],{"class":187},[177,1096,607],{"class":230},[177,1098,638],{"class":187},[177,1100,1102,1104,1107,1109,1111,1114,1116,1118],{"class":179,"line":1101},43,[177,1103,569],{"class":183},[177,1105,1106],{"class":230}," decipher",[177,1108,234],{"class":183},[177,1110,577],{"class":187},[177,1112,1113],{"class":237},"createDecipheriv",[177,1115,241],{"class":187},[177,1117,664],{"class":194},[177,1119,667],{"class":187},[177,1121,1123,1126,1129],{"class":179,"line":1122},44,[177,1124,1125],{"class":187},"    decipher.",[177,1127,1128],{"class":237},"setAuthTag",[177,1130,1131],{"class":187},"(tag);\n",[177,1133,1135],{"class":179,"line":1134},45,[177,1136,218],{"emptyLinePlaceholder":30},[177,1138,1140,1142,1144,1146,1149,1151],{"class":179,"line":1139},46,[177,1141,728],{"class":183},[177,1143,756],{"class":183},[177,1145,533],{"class":237},[177,1147,1148],{"class":187},"(Buffer.",[177,1150,683],{"class":237},[177,1152,1153],{"class":187},"([\n",[177,1155,1157,1160,1162],{"class":179,"line":1156},47,[177,1158,1159],{"class":187},"      decipher.",[177,1161,689],{"class":237},[177,1163,1164],{"class":187},"(ciphertext),\n",[177,1166,1168,1170,1172],{"class":179,"line":1167},48,[177,1169,1159],{"class":187},[177,1171,695],{"class":237},[177,1173,1174],{"class":187},"()\n",[177,1176,1178],{"class":179,"line":1177},49,[177,1179,1180],{"class":187},"    ]));\n",[177,1182,1184,1186,1188,1190,1193,1195,1198],{"class":179,"line":1183},50,[177,1185,741],{"class":187},[177,1187,744],{"class":183},[177,1189,523],{"class":187},[177,1191,1192],{"class":526},"error",[177,1194,530],{"class":183},[177,1196,1197],{"class":230}," unknown",[177,1199,1200],{"class":187},") {\n",[177,1202,1204,1206,1209,1211,1214,1217,1219,1222,1224,1226,1229],{"class":179,"line":1203},51,[177,1205,569],{"class":183},[177,1207,1208],{"class":230}," err",[177,1210,234],{"class":183},[177,1212,1213],{"class":187}," error ",[177,1215,1216],{"class":183},"instanceof",[177,1218,759],{"class":237},[177,1220,1221],{"class":183}," ?",[177,1223,1213],{"class":187},[177,1225,530],{"class":183},[177,1227,1228],{"class":230}," null",[177,1230,198],{"class":187},[177,1232,1234,1236,1239,1241,1244,1247,1250,1252,1255],{"class":179,"line":1233},52,[177,1235,569],{"class":183},[177,1237,1238],{"class":230}," code",[177,1240,234],{"class":183},[177,1242,1243],{"class":187}," (error ",[177,1245,1246],{"class":183},"as",[177,1248,1249],{"class":237}," NodeJS",[177,1251,65],{"class":187},[177,1253,1254],{"class":237},"ErrnoException",[177,1256,1257],{"class":187},").code;\n",[177,1259,1261,1263,1266,1269,1272,1275,1278,1281,1283,1286,1288,1291],{"class":179,"line":1260},53,[177,1262,932],{"class":183},[177,1264,1265],{"class":187}," (code ",[177,1267,1268],{"class":183},"===",[177,1270,1271],{"class":194}," 'ERR_CRYPTO_OPERATION_FAILED'",[177,1273,1274],{"class":183}," ||",[177,1276,1277],{"class":187}," err?.message.",[177,1279,1280],{"class":237},"toLowerCase",[177,1282,367],{"class":187},[177,1284,1285],{"class":237},"includes",[177,1287,241],{"class":187},[177,1289,1290],{"class":194},"'unsupported state'",[177,1292,1293],{"class":187},")) {\n",[177,1295,1297,1299,1301,1303,1305,1308],{"class":179,"line":1296},54,[177,1298,950],{"class":183},[177,1300,756],{"class":183},[177,1302,759],{"class":237},[177,1304,241],{"class":187},[177,1306,1307],{"class":194},"'Incorrect password or data'",[177,1309,588],{"class":187},[177,1311,1313],{"class":179,"line":1312},55,[177,1314,967],{"class":187},[177,1316,1318,1321,1324,1326,1329],{"class":179,"line":1317},56,[177,1319,1320],{"class":187},"    consola.",[177,1322,1323],{"class":237},"log",[177,1325,241],{"class":187},[177,1327,1328],{"class":194},"`Decryption failure`",[177,1330,588],{"class":187},[177,1332,1334,1336],{"class":179,"line":1333},57,[177,1335,753],{"class":183},[177,1337,1338],{"class":187}," error;\n",[177,1340,1342],{"class":179,"line":1341},58,[177,1343,790],{"class":187},[177,1345,1347],{"class":179,"line":1346},59,[177,1348,501],{"class":187},[17,1350,1351,1352,1355,1356,1359,1360,1365],{},"Now every time a new sub-wallet is created, or a main wallet is stored or updated I call ",[174,1353,1354],{},"encryptPrivateKey"," when I transfer funds, make transactions, etc, I call ",[174,1357,1358],{},"decryptPrivateKey",", perfect. This pair can probably be fed also to my ",[53,1361,1364],{"href":1362,"rel":1363},"https://github.com/Sergo706/utils",[57],"utils"," package later for some other weird use case I guess.",[17,1367,1368],{},"However the most notable problems with the above approach are:",[412,1370,1371,1374],{},[415,1372,1373],{},"There is no recovery mechanism, if you lose your password, you lose access to your wallets.",[415,1375,1376],{},"You need to provide your password every time you make sensitive actions, which can be insecure in cli contexts (AI tools, background processes, etc.).",[17,1378,1379],{},"But that is still more than enough for its purpose.",[17,1381,1382,1383,1386,1387,1394],{},"Then the code for creating wallets becomes really simple you just call ",[174,1384,1385],{},"Keypair.generate()"," method from the ",[53,1388,1391],{"href":1389,"rel":1390},"https://github.com/solana-foundation/solana-web3.js",[57],[174,1392,1393],{},"@solana/web3.js"," package:",[167,1396,1399],{"className":169,"code":1397,"filename":1398,"language":172,"meta":15,"style":15},"export async function createWallet(password: string) {\n    const newWallet = Keypair.generate();\n    const encP = encryptPrivateKey(newWallet.secretKey, password);\n\n   await db.insert(walletTable).values({\n        publicAddress: newWallet.publicKey.toBase58(),\n        privateKeyEncrypted: encP,\n        isMainWallet: false\n    });\n\n    consola.success('New Wallet created successfully', newWallet.publicKey);\n    return;\n}\n","createWallet.ts",[174,1400,1401,1424,1441,1455,1459,1479,1489,1494,1502,1507,1511,1526,1532],{"__ignoreMap":15},[177,1402,1403,1405,1408,1411,1414,1416,1418,1420,1422],{"class":179,"line":180},[177,1404,224],{"class":183},[177,1406,1407],{"class":183}," async",[177,1409,1410],{"class":183}," function",[177,1412,1413],{"class":237}," createWallet",[177,1415,241],{"class":187},[177,1417,539],{"class":526},[177,1419,530],{"class":183},[177,1421,544],{"class":230},[177,1423,1200],{"class":187},[177,1425,1426,1428,1431,1433,1436,1439],{"class":179,"line":25},[177,1427,569],{"class":183},[177,1429,1430],{"class":230}," newWallet",[177,1432,234],{"class":183},[177,1434,1435],{"class":187}," Keypair.",[177,1437,1438],{"class":237},"generate",[177,1440,717],{"class":187},[177,1442,1443,1445,1448,1450,1452],{"class":179,"line":215},[177,1444,569],{"class":183},[177,1446,1447],{"class":230}," encP",[177,1449,234],{"class":183},[177,1451,518],{"class":237},[177,1453,1454],{"class":187},"(newWallet.secretKey, password);\n",[177,1456,1457],{"class":179,"line":221},[177,1458,218],{"emptyLinePlaceholder":30},[177,1460,1461,1464,1467,1470,1473,1476],{"class":179,"line":250},[177,1462,1463],{"class":183},"   await",[177,1465,1466],{"class":187}," db.",[177,1468,1469],{"class":237},"insert",[177,1471,1472],{"class":187},"(walletTable).",[177,1474,1475],{"class":237},"values",[177,1477,1478],{"class":187},"({\n",[177,1480,1481,1484,1487],{"class":179,"line":285},[177,1482,1483],{"class":187},"        publicAddress: newWallet.publicKey.",[177,1485,1486],{"class":237},"toBase58",[177,1488,305],{"class":187},[177,1490,1491],{"class":179,"line":308},[177,1492,1493],{"class":187},"        privateKeyEncrypted: encP,\n",[177,1495,1496,1499],{"class":179,"line":329},[177,1497,1498],{"class":187},"        isMainWallet: ",[177,1500,1501],{"class":230},"false\n",[177,1503,1504],{"class":179,"line":350},[177,1505,1506],{"class":187},"    });\n",[177,1508,1509],{"class":179,"line":384},[177,1510,218],{"emptyLinePlaceholder":30},[177,1512,1513,1515,1518,1520,1523],{"class":179,"line":591},[177,1514,1320],{"class":187},[177,1516,1517],{"class":237},"success",[177,1519,241],{"class":187},[177,1521,1522],{"class":194},"'New Wallet created successfully'",[177,1524,1525],{"class":187},", newWallet.publicKey);\n",[177,1527,1528,1530],{"class":179,"line":613},[177,1529,728],{"class":183},[177,1531,198],{"class":187},[177,1533,1534],{"class":179,"line":641},[177,1535,1536],{"class":187},"}\n",[17,1538,1539,1540,1543,1544,1547],{},"For storing the main wallet, I thought to let a user provide a file path, since its a cli right now. And because the format of the key can come as a ",[174,1541,1542],{},"base58"," and ",[174,1545,1546],{},"Uint8Array"," the code should detect that, try to parse it, and forbid overriding an existing main key if one already exists:",[167,1549,1551],{"className":169,"code":1550,"filename":1398,"language":172,"meta":15,"style":15},"export async function storeMain(password: string, secretPath: string) {\n    const [main] = await db.select().from(walletTable).where(eq(walletTable.isMainWallet, true));\n\n    if (main?.privateKeyEncrypted || main?.publicAddress) {\n        consola.error(`MAIN wallet already exists!`);\n        throw new Error(`MAIN wallet already exists!`);\n    }\n    \n    const filePath = path.resolve(process.cwd(), secretPath);\n    const secretKeyString = fs.readFileSync(filePath, \"utf8\");\n\n    if (!secretKeyString) {\n        consola.error(`File path didn't found at ${secretPath}`);\n        throw new Error(`File path didn't found at ${secretPath}`);\n    }\n\n    try {\n        const trimmed = secretKeyString.trim();\n        let secretKeyBytes: Uint8Array;\n\n        try {\n            const parsed = JSON.parse(trimmed)  as number[] | string;\n            if (Array.isArray(parsed) && parsed.every((n) => typeof n === 'number')) {\n                secretKeyBytes = Uint8Array.from(parsed);\n\n            } else if (typeof parsed === 'string') {\n                secretKeyBytes = bs58.decode(parsed);\n            } else {\n                throw new Error('Unsupported JSON secret format');\n            }\n\n        } catch {\n            secretKeyBytes = bs58.decode(trimmed);\n        }\n\n        const pub = Keypair.fromSecretKey(secretKeyBytes);\n        const encP = encryptPrivateKey(pub.secretKey, password);\n\n        await db.insert(walletTable).values({\n            publicAddress: pub.publicKey.toBase58(),\n            privateKeyEncrypted: encP,\n            isMainWallet: true\n        });\n\n        consola.success('Main Wallet stored successfully', pub.publicKey.toBase58());\n        return;\n    } catch (err) {\n        consola.error(\"Invalid secret key format. Ensure it is a JSON array of numbers, a base58 string, base64, or hex.\");\n        consola.error(err);\n        throw new Error(\"Invalid secret key format. Ensure it is a JSON array of numbers, a base58 string, base64, or hex.\");\n    }\n}\n",[174,1552,1553,1583,1628,1632,1645,1659,1674,1678,1682,1706,1729,1733,1745,1762,1780,1784,1788,1795,1813,1827,1831,1838,1874,1922,1937,1941,1967,1981,1989,2005,2010,2014,2023,2037,2042,2046,2063,2076,2080,2095,2104,2109,2117,2122,2126,2145,2152,2161,2174,2183,2197,2201],{"__ignoreMap":15},[177,1554,1555,1557,1559,1561,1564,1566,1568,1570,1572,1574,1577,1579,1581],{"class":179,"line":180},[177,1556,224],{"class":183},[177,1558,1407],{"class":183},[177,1560,1410],{"class":183},[177,1562,1563],{"class":237}," storeMain",[177,1565,241],{"class":187},[177,1567,539],{"class":526},[177,1569,530],{"class":183},[177,1571,544],{"class":230},[177,1573,536],{"class":187},[177,1575,1576],{"class":526},"secretPath",[177,1578,530],{"class":183},[177,1580,544],{"class":230},[177,1582,1200],{"class":187},[177,1584,1585,1587,1590,1593,1596,1598,1601,1603,1606,1608,1610,1612,1615,1617,1620,1623,1625],{"class":179,"line":25},[177,1586,569],{"class":183},[177,1588,1589],{"class":187}," [",[177,1591,1592],{"class":230},"main",[177,1594,1595],{"class":187},"] ",[177,1597,984],{"class":183},[177,1599,1600],{"class":183}," await",[177,1602,1466],{"class":187},[177,1604,1605],{"class":237},"select",[177,1607,367],{"class":187},[177,1609,191],{"class":237},[177,1611,1472],{"class":187},[177,1613,1614],{"class":237},"where",[177,1616,241],{"class":187},[177,1618,1619],{"class":237},"eq",[177,1621,1622],{"class":187},"(walletTable.isMainWallet, ",[177,1624,279],{"class":230},[177,1626,1627],{"class":187},"));\n",[177,1629,1630],{"class":179,"line":215},[177,1631,218],{"emptyLinePlaceholder":30},[177,1633,1634,1636,1639,1642],{"class":179,"line":221},[177,1635,932],{"class":183},[177,1637,1638],{"class":187}," (main?.privateKeyEncrypted ",[177,1640,1641],{"class":183},"||",[177,1643,1644],{"class":187}," main?.publicAddress) {\n",[177,1646,1647,1650,1652,1654,1657],{"class":179,"line":250},[177,1648,1649],{"class":187},"        consola.",[177,1651,1192],{"class":237},[177,1653,241],{"class":187},[177,1655,1656],{"class":194},"`MAIN wallet already exists!`",[177,1658,588],{"class":187},[177,1660,1661,1664,1666,1668,1670,1672],{"class":179,"line":285},[177,1662,1663],{"class":183},"        throw",[177,1665,756],{"class":183},[177,1667,759],{"class":237},[177,1669,241],{"class":187},[177,1671,1656],{"class":194},[177,1673,588],{"class":187},[177,1675,1676],{"class":179,"line":308},[177,1677,967],{"class":187},[177,1679,1680],{"class":179,"line":329},[177,1681,644],{"class":187},[177,1683,1684,1686,1689,1691,1694,1697,1700,1703],{"class":179,"line":350},[177,1685,569],{"class":183},[177,1687,1688],{"class":230}," filePath",[177,1690,234],{"class":183},[177,1692,1693],{"class":187}," path.",[177,1695,1696],{"class":237},"resolve",[177,1698,1699],{"class":187},"(process.",[177,1701,1702],{"class":237},"cwd",[177,1704,1705],{"class":187},"(), secretPath);\n",[177,1707,1708,1710,1713,1715,1718,1721,1724,1727],{"class":179,"line":384},[177,1709,569],{"class":183},[177,1711,1712],{"class":230}," secretKeyString",[177,1714,234],{"class":183},[177,1716,1717],{"class":187}," fs.",[177,1719,1720],{"class":237},"readFileSync",[177,1722,1723],{"class":187},"(filePath, ",[177,1725,1726],{"class":194},"\"utf8\"",[177,1728,588],{"class":187},[177,1730,1731],{"class":179,"line":591},[177,1732,218],{"emptyLinePlaceholder":30},[177,1734,1735,1737,1739,1742],{"class":179,"line":613},[177,1736,932],{"class":183},[177,1738,523],{"class":187},[177,1740,1741],{"class":183},"!",[177,1743,1744],{"class":187},"secretKeyString) {\n",[177,1746,1747,1749,1751,1753,1756,1758,1760],{"class":179,"line":641},[177,1748,1649],{"class":187},[177,1750,1192],{"class":237},[177,1752,241],{"class":187},[177,1754,1755],{"class":194},"`File path didn't found at ${",[177,1757,1576],{"class":187},[177,1759,782],{"class":194},[177,1761,588],{"class":187},[177,1763,1764,1766,1768,1770,1772,1774,1776,1778],{"class":179,"line":647},[177,1765,1663],{"class":183},[177,1767,756],{"class":183},[177,1769,759],{"class":237},[177,1771,241],{"class":187},[177,1773,1755],{"class":194},[177,1775,1576],{"class":187},[177,1777,782],{"class":194},[177,1779,588],{"class":187},[177,1781,1782],{"class":179,"line":670},[177,1783,967],{"class":187},[177,1785,1786],{"class":179,"line":701},[177,1787,218],{"emptyLinePlaceholder":30},[177,1789,1790,1793],{"class":179,"line":720},[177,1791,1792],{"class":183},"    try",[177,1794,453],{"class":187},[177,1796,1797,1800,1803,1805,1808,1811],{"class":179,"line":725},[177,1798,1799],{"class":183},"        const",[177,1801,1802],{"class":230}," trimmed",[177,1804,234],{"class":183},[177,1806,1807],{"class":187}," secretKeyString.",[177,1809,1810],{"class":237},"trim",[177,1812,717],{"class":187},[177,1814,1815,1818,1821,1823,1825],{"class":179,"line":738},[177,1816,1817],{"class":183},"        let",[177,1819,1820],{"class":187}," secretKeyBytes",[177,1822,530],{"class":183},[177,1824,533],{"class":237},[177,1826,198],{"class":187},[177,1828,1829],{"class":179,"line":750},[177,1830,218],{"emptyLinePlaceholder":30},[177,1832,1833,1836],{"class":179,"line":787},[177,1834,1835],{"class":183},"        try",[177,1837,453],{"class":187},[177,1839,1840,1843,1846,1848,1851,1853,1856,1859,1861,1864,1867,1870,1872],{"class":179,"line":793},[177,1841,1842],{"class":183},"            const",[177,1844,1845],{"class":230}," parsed",[177,1847,234],{"class":183},[177,1849,1850],{"class":230}," JSON",[177,1852,65],{"class":187},[177,1854,1855],{"class":237},"parse",[177,1857,1858],{"class":187},"(trimmed)  ",[177,1860,1246],{"class":183},[177,1862,1863],{"class":230}," number",[177,1865,1866],{"class":187},"[] ",[177,1868,1869],{"class":183},"|",[177,1871,544],{"class":230},[177,1873,198],{"class":187},[177,1875,1876,1879,1882,1885,1888,1891,1894,1897,1900,1903,1906,1909,1912,1915,1917,1920],{"class":179,"line":798},[177,1877,1878],{"class":183},"            if",[177,1880,1881],{"class":187}," (Array.",[177,1883,1884],{"class":237},"isArray",[177,1886,1887],{"class":187},"(parsed) ",[177,1889,1890],{"class":183},"&&",[177,1892,1893],{"class":187}," parsed.",[177,1895,1896],{"class":237},"every",[177,1898,1899],{"class":187},"((",[177,1901,1902],{"class":526},"n",[177,1904,1905],{"class":187},") ",[177,1907,1908],{"class":183},"=>",[177,1910,1911],{"class":183}," typeof",[177,1913,1914],{"class":187}," n ",[177,1916,1268],{"class":183},[177,1918,1919],{"class":194}," 'number'",[177,1921,1293],{"class":187},[177,1923,1924,1927,1929,1932,1934],{"class":179,"line":803},[177,1925,1926],{"class":187},"                secretKeyBytes ",[177,1928,984],{"class":183},[177,1930,1931],{"class":187}," Uint8Array.",[177,1933,191],{"class":237},[177,1935,1936],{"class":187},"(parsed);\n",[177,1938,1939],{"class":179,"line":842},[177,1940,218],{"emptyLinePlaceholder":30},[177,1942,1943,1946,1949,1952,1954,1957,1960,1962,1965],{"class":179,"line":849},[177,1944,1945],{"class":187},"            } ",[177,1947,1948],{"class":183},"else",[177,1950,1951],{"class":183}," if",[177,1953,523],{"class":187},[177,1955,1956],{"class":183},"typeof",[177,1958,1959],{"class":187}," parsed ",[177,1961,1268],{"class":183},[177,1963,1964],{"class":194}," 'string'",[177,1966,1200],{"class":187},[177,1968,1969,1971,1973,1976,1979],{"class":179,"line":854},[177,1970,1926],{"class":187},[177,1972,984],{"class":183},[177,1974,1975],{"class":187}," bs58.",[177,1977,1978],{"class":237},"decode",[177,1980,1936],{"class":187},[177,1982,1983,1985,1987],{"class":179,"line":869},[177,1984,1945],{"class":187},[177,1986,1948],{"class":183},[177,1988,453],{"class":187},[177,1990,1991,1994,1996,1998,2000,2003],{"class":179,"line":885},[177,1992,1993],{"class":183},"                throw",[177,1995,756],{"class":183},[177,1997,759],{"class":237},[177,1999,241],{"class":187},[177,2001,2002],{"class":194},"'Unsupported JSON secret format'",[177,2004,588],{"class":187},[177,2006,2007],{"class":179,"line":900},[177,2008,2009],{"class":187},"            }\n",[177,2011,2012],{"class":179,"line":924},[177,2013,218],{"emptyLinePlaceholder":30},[177,2015,2016,2019,2021],{"class":179,"line":929},[177,2017,2018],{"class":187},"        } ",[177,2020,744],{"class":183},[177,2022,453],{"class":187},[177,2024,2025,2028,2030,2032,2034],{"class":179,"line":947},[177,2026,2027],{"class":187},"            secretKeyBytes ",[177,2029,984],{"class":183},[177,2031,1975],{"class":187},[177,2033,1978],{"class":237},[177,2035,2036],{"class":187},"(trimmed);\n",[177,2038,2039],{"class":179,"line":964},[177,2040,2041],{"class":187},"        }\n",[177,2043,2044],{"class":179,"line":970},[177,2045,218],{"emptyLinePlaceholder":30},[177,2047,2048,2050,2053,2055,2057,2060],{"class":179,"line":975},[177,2049,1799],{"class":183},[177,2051,2052],{"class":230}," pub",[177,2054,234],{"class":183},[177,2056,1435],{"class":187},[177,2058,2059],{"class":237},"fromSecretKey",[177,2061,2062],{"class":187},"(secretKeyBytes);\n",[177,2064,2065,2067,2069,2071,2073],{"class":179,"line":992},[177,2066,1799],{"class":183},[177,2068,1447],{"class":230},[177,2070,234],{"class":183},[177,2072,518],{"class":237},[177,2074,2075],{"class":187},"(pub.secretKey, password);\n",[177,2077,2078],{"class":179,"line":1016},[177,2079,218],{"emptyLinePlaceholder":30},[177,2081,2082,2085,2087,2089,2091,2093],{"class":179,"line":1036},[177,2083,2084],{"class":183},"        await",[177,2086,1466],{"class":187},[177,2088,1469],{"class":237},[177,2090,1472],{"class":187},[177,2092,1475],{"class":237},[177,2094,1478],{"class":187},[177,2096,2097,2100,2102],{"class":179,"line":1056},[177,2098,2099],{"class":187},"            publicAddress: pub.publicKey.",[177,2101,1486],{"class":237},[177,2103,305],{"class":187},[177,2105,2106],{"class":179,"line":1073},[177,2107,2108],{"class":187},"            privateKeyEncrypted: encP,\n",[177,2110,2111,2114],{"class":179,"line":1078},[177,2112,2113],{"class":187},"            isMainWallet: ",[177,2115,2116],{"class":230},"true\n",[177,2118,2119],{"class":179,"line":1101},[177,2120,2121],{"class":187},"        });\n",[177,2123,2124],{"class":179,"line":1122},[177,2125,218],{"emptyLinePlaceholder":30},[177,2127,2128,2130,2132,2134,2137,2140,2142],{"class":179,"line":1134},[177,2129,1649],{"class":187},[177,2131,1517],{"class":237},[177,2133,241],{"class":187},[177,2135,2136],{"class":194},"'Main Wallet stored successfully'",[177,2138,2139],{"class":187},", pub.publicKey.",[177,2141,1486],{"class":237},[177,2143,2144],{"class":187},"());\n",[177,2146,2147,2150],{"class":179,"line":1139},[177,2148,2149],{"class":183},"        return",[177,2151,198],{"class":187},[177,2153,2154,2157,2159],{"class":179,"line":1156},[177,2155,2156],{"class":187},"    } ",[177,2158,744],{"class":183},[177,2160,747],{"class":187},[177,2162,2163,2165,2167,2169,2172],{"class":179,"line":1167},[177,2164,1649],{"class":187},[177,2166,1192],{"class":237},[177,2168,241],{"class":187},[177,2170,2171],{"class":194},"\"Invalid secret key format. Ensure it is a JSON array of numbers, a base58 string, base64, or hex.\"",[177,2173,588],{"class":187},[177,2175,2176,2178,2180],{"class":179,"line":1177},[177,2177,1649],{"class":187},[177,2179,1192],{"class":237},[177,2181,2182],{"class":187},"(err);\n",[177,2184,2185,2187,2189,2191,2193,2195],{"class":179,"line":1183},[177,2186,1663],{"class":183},[177,2188,756],{"class":183},[177,2190,759],{"class":237},[177,2192,241],{"class":187},[177,2194,2171],{"class":194},[177,2196,588],{"class":187},[177,2198,2199],{"class":179,"line":1203},[177,2200,967],{"class":187},[177,2202,2203],{"class":179,"line":1233},[177,2204,1536],{"class":187},[17,2206,2207],{},"At this point I got the foundation of the bots set up.",[412,2209,2210,2213,2216,2219],{},[415,2211,2212],{},"Only one main wallet can exist at a time",[415,2214,2215],{},"Sub wallets can be created as much as you want (eg the bots)",[415,2217,2218],{},"Everything is encrypted and decrypted on demand",[415,2220,2221],{},"Everything except the user password is stored in sqlite",[17,2223,2224],{},"I got up and continued with the wallets logic:",[412,2226,2227,2233,2239,2245],{},[415,2228,2229,2232],{},[174,2230,2231],{},"drainWalletsToMain"," to drain all sub-wallets funds back to the main wallet, that accepts the user password for decryption, calculates the solana fees and skips wallets that don't have enough lamports (or SOL. 1 SOL === 1,000,000,000).",[415,2234,2235,2238],{},[174,2236,2237],{},"distributeGas"," to split SOL to sub-wallets that accepts also the password for decryption, and the amount of the SOL to split to each wallet. This one calculates the total fees for all transactions that will be performed + the specified amount if there is not enough SOL it throws.",[415,2240,2241,2244],{},[174,2242,2243],{},"getByPubkey"," To read metadata by a specific public key of a wallet such as SOL in, creation date etc.",[415,2246,2247,2250,2251,2253],{},[174,2248,2249],{},"getAllWalletsView"," Same as ",[174,2252,2243],{}," but to get the view of all wallets, including the main one.",[17,2255,2256],{},"Both also accept the password.",[404,2258,2260],{"id":2259},"setting-up-the-trade-logic","Setting up the Trade Logic",[17,2262,2263],{},"With the wallet functions wired, I first focused heavily on the core features:",[412,2265,2266,2269,2272],{},[415,2267,2268],{},"Buy: To start a mass buy using the sub-wallets with configurable wallet amounts to use, SOL amounts to use from each wallet, and the target mint.",[415,2270,2271],{},"Sell: To start a mass sell, also with the number of wallets to use, the target mint, and the percentage to sell from each wallet.",[415,2273,2274],{},"Out: To panic out from all positions.",[17,2276,2277],{},"All operations also accept the password to decrypt the wallets that are going to be used.",[17,2279,2280],{},"Because the CLI runs entirely locally, and needs to manage user specific apis, I made another table:",[167,2282,2285],{"className":169,"code":2283,"filename":2284,"language":172,"meta":15,"style":15},"import { blob, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { sql } from \"drizzle-orm\";\n\nexport const apiAndRpcTable = sqliteTable('api_rpc', {\n    id: integer('id', {mode: 'number'}).primaryKey({ autoIncrement: true }),\n    createdAt: integer('created_at').notNull().default(sql`(cast(unixepoch() as int))`),\n    encryptedApiKey: blob('private_key_encrypted').notNull(),\n    rpc: text('rpc', {mode: 'text' }).notNull(),\n});\n","secrets.ts",[174,2286,2287,2299,2311,2315,2335,2359,2385,2402,2426],{"__ignoreMap":15},[177,2288,2289,2291,2293,2295,2297],{"class":179,"line":180},[177,2290,184],{"class":183},[177,2292,188],{"class":187},[177,2294,191],{"class":183},[177,2296,195],{"class":194},[177,2298,198],{"class":187},[177,2300,2301,2303,2305,2307,2309],{"class":179,"line":25},[177,2302,184],{"class":183},[177,2304,205],{"class":187},[177,2306,191],{"class":183},[177,2308,210],{"class":194},[177,2310,198],{"class":187},[177,2312,2313],{"class":179,"line":215},[177,2314,218],{"emptyLinePlaceholder":30},[177,2316,2317,2319,2321,2324,2326,2328,2330,2333],{"class":179,"line":221},[177,2318,224],{"class":183},[177,2320,227],{"class":183},[177,2322,2323],{"class":230}," apiAndRpcTable",[177,2325,234],{"class":183},[177,2327,238],{"class":237},[177,2329,241],{"class":187},[177,2331,2332],{"class":194},"'api_rpc'",[177,2334,247],{"class":187},[177,2336,2337,2339,2341,2343,2345,2347,2349,2351,2353,2355,2357],{"class":179,"line":250},[177,2338,253],{"class":187},[177,2340,256],{"class":237},[177,2342,241],{"class":187},[177,2344,261],{"class":194},[177,2346,264],{"class":187},[177,2348,267],{"class":194},[177,2350,270],{"class":187},[177,2352,273],{"class":237},[177,2354,276],{"class":187},[177,2356,279],{"class":230},[177,2358,282],{"class":187},[177,2360,2361,2363,2365,2367,2369,2371,2373,2375,2377,2379,2381,2383],{"class":179,"line":285},[177,2362,353],{"class":187},[177,2364,256],{"class":237},[177,2366,241],{"class":187},[177,2368,360],{"class":194},[177,2370,299],{"class":187},[177,2372,345],{"class":237},[177,2374,367],{"class":187},[177,2376,370],{"class":237},[177,2378,241],{"class":187},[177,2380,375],{"class":237},[177,2382,378],{"class":194},[177,2384,381],{"class":187},[177,2386,2387,2390,2392,2394,2396,2398,2400],{"class":179,"line":308},[177,2388,2389],{"class":187},"    encryptedApiKey: ",[177,2391,335],{"class":237},[177,2393,241],{"class":187},[177,2395,340],{"class":194},[177,2397,299],{"class":187},[177,2399,345],{"class":237},[177,2401,305],{"class":187},[177,2403,2404,2407,2409,2411,2414,2416,2419,2422,2424],{"class":179,"line":329},[177,2405,2406],{"class":187},"    rpc: ",[177,2408,291],{"class":237},[177,2410,241],{"class":187},[177,2412,2413],{"class":194},"'rpc'",[177,2415,264],{"class":187},[177,2417,2418],{"class":194},"'text'",[177,2420,2421],{"class":187}," }).",[177,2423,345],{"class":237},[177,2425,305],{"class":187},[177,2427,2428],{"class":179,"line":350},[177,2429,387],{"class":187},[17,2431,2432,2433,2436],{},"The ",[174,2434,2435],{},"encryptedApiKey"," field is the Jupiter API key, which is also encrypted using the mechanism described above, and decrypted with the password inside the trade operations.",[17,2438,2432,2439,2442],{},[174,2440,2441],{},"rpc"," field is the URL of the RPC provider being used; this field is not encrypted.",[17,2444,2445,2446,2451],{},"While I am not going to paste all the code for these functions in this post (you can find it ",[53,2447,2450],{"href":2448,"rel":2449},"https://github.com/Sergo706/solana-bots/tree/main/src/lib/transactions",[57],"here","), the main challenge I found difficult to solve was rate limits.",[17,2453,2454],{},"The solutions were either to use a paid provider or to break down the logic to be rarely rate-limited by the public RPC.",[17,2456,2457],{},"You already guessed it, I chose the second option. To stop the bot from spamming the RPC and Jupiter API all at once, I wrote a custom fetch wrapper. It catches 429 errors and applies an exponential backoff with a bit of random jitter before it retries.",[167,2459,2462],{"className":169,"code":2460,"filename":2461,"language":172,"meta":15,"style":15},"import consola from \"consola\";\n\nexport async function fetchWithRetry(url: string, retries = 5, delay = 1000, init?: RequestInit) {\n  try {\n    const res = await fetch(url, init);\n    if (res.status === 429 && retries > 0) {\n\n      const jitter = Math.random() * (delay * 0.5);\n      const totalWait = delay + jitter;\n\n      consola.warn(`[429] Rate limited on ${url}. Retrying in ${(totalWait / 1000).toFixed(2)}s...`);\n      await new Promise(res => setTimeout(res, totalWait));\n      return await fetchWithRetry(url, retries - 1, delay * 2, init);\n    }\n    \n    return res;\n  } catch (err) {\n    if (retries > 0) {\n      consola.error(`[Network Error] ${url}. Retrying...`);\n      return fetchWithRetry(url, retries - 1, delay * 2, init);\n    }\n    throw err;\n  }\n}\n","fetchWithRetry.ts",[174,2463,2464,2478,2482,2535,2541,2558,2583,2587,2619,2636,2640,2685,2708,2737,2741,2745,2752,2760,2773,2791,2811,2815,2822,2826],{"__ignoreMap":15},[177,2465,2466,2468,2471,2473,2476],{"class":179,"line":180},[177,2467,184],{"class":183},[177,2469,2470],{"class":187}," consola ",[177,2472,191],{"class":183},[177,2474,2475],{"class":194}," \"consola\"",[177,2477,198],{"class":187},[177,2479,2480],{"class":179,"line":25},[177,2481,218],{"emptyLinePlaceholder":30},[177,2483,2484,2486,2488,2490,2493,2495,2498,2500,2502,2504,2507,2509,2512,2514,2517,2519,2522,2524,2527,2530,2533],{"class":179,"line":215},[177,2485,224],{"class":183},[177,2487,1407],{"class":183},[177,2489,1410],{"class":183},[177,2491,2492],{"class":237}," fetchWithRetry",[177,2494,241],{"class":187},[177,2496,2497],{"class":526},"url",[177,2499,530],{"class":183},[177,2501,544],{"class":230},[177,2503,536],{"class":187},[177,2505,2506],{"class":526},"retries",[177,2508,234],{"class":183},[177,2510,2511],{"class":230}," 5",[177,2513,536],{"class":187},[177,2515,2516],{"class":526},"delay",[177,2518,234],{"class":183},[177,2520,2521],{"class":230}," 1000",[177,2523,536],{"class":187},[177,2525,2526],{"class":526},"init",[177,2528,2529],{"class":183},"?:",[177,2531,2532],{"class":237}," RequestInit",[177,2534,1200],{"class":187},[177,2536,2537,2539],{"class":179,"line":221},[177,2538,562],{"class":183},[177,2540,453],{"class":187},[177,2542,2543,2545,2548,2550,2552,2555],{"class":179,"line":250},[177,2544,569],{"class":183},[177,2546,2547],{"class":230}," res",[177,2549,234],{"class":183},[177,2551,1600],{"class":183},[177,2553,2554],{"class":237}," fetch",[177,2556,2557],{"class":187},"(url, init);\n",[177,2559,2560,2562,2565,2567,2570,2573,2576,2579,2581],{"class":179,"line":285},[177,2561,932],{"class":183},[177,2563,2564],{"class":187}," (res.status ",[177,2566,1268],{"class":183},[177,2568,2569],{"class":230}," 429",[177,2571,2572],{"class":183}," &&",[177,2574,2575],{"class":187}," retries ",[177,2577,2578],{"class":183},">",[177,2580,987],{"class":230},[177,2582,1200],{"class":187},[177,2584,2585],{"class":179,"line":308},[177,2586,218],{"emptyLinePlaceholder":30},[177,2588,2589,2592,2595,2597,2600,2603,2606,2609,2612,2614,2617],{"class":179,"line":329},[177,2590,2591],{"class":183},"      const",[177,2593,2594],{"class":230}," jitter",[177,2596,234],{"class":183},[177,2598,2599],{"class":187}," Math.",[177,2601,2602],{"class":237},"random",[177,2604,2605],{"class":187},"() ",[177,2607,2608],{"class":183},"*",[177,2610,2611],{"class":187}," (delay ",[177,2613,2608],{"class":183},[177,2615,2616],{"class":230}," 0.5",[177,2618,588],{"class":187},[177,2620,2621,2623,2626,2628,2631,2633],{"class":179,"line":350},[177,2622,2591],{"class":183},[177,2624,2625],{"class":230}," totalWait",[177,2627,234],{"class":183},[177,2629,2630],{"class":187}," delay ",[177,2632,913],{"class":183},[177,2634,2635],{"class":187}," jitter;\n",[177,2637,2638],{"class":179,"line":384},[177,2639,218],{"emptyLinePlaceholder":30},[177,2641,2642,2645,2648,2650,2653,2655,2658,2660,2663,2666,2668,2670,2673,2675,2678,2680,2683],{"class":179,"line":591},[177,2643,2644],{"class":187},"      consola.",[177,2646,2647],{"class":237},"warn",[177,2649,241],{"class":187},[177,2651,2652],{"class":194},"`[429] Rate limited on ${",[177,2654,2497],{"class":187},[177,2656,2657],{"class":194},"}. Retrying in ${",[177,2659,241],{"class":194},[177,2661,2662],{"class":187},"totalWait",[177,2664,2665],{"class":183}," /",[177,2667,2521],{"class":230},[177,2669,299],{"class":194},[177,2671,2672],{"class":237},"toFixed",[177,2674,241],{"class":194},[177,2676,2677],{"class":230},"2",[177,2679,547],{"class":194},[177,2681,2682],{"class":194},"}s...`",[177,2684,588],{"class":187},[177,2686,2687,2690,2692,2695,2697,2700,2702,2705],{"class":179,"line":613},[177,2688,2689],{"class":183},"      await",[177,2691,756],{"class":183},[177,2693,2694],{"class":230}," Promise",[177,2696,241],{"class":187},[177,2698,2699],{"class":526},"res",[177,2701,555],{"class":183},[177,2703,2704],{"class":237}," setTimeout",[177,2706,2707],{"class":187},"(res, totalWait));\n",[177,2709,2710,2713,2715,2717,2720,2723,2726,2729,2731,2734],{"class":179,"line":641},[177,2711,2712],{"class":183},"      return",[177,2714,1600],{"class":183},[177,2716,2492],{"class":237},[177,2718,2719],{"class":187},"(url, retries ",[177,2721,2722],{"class":183},"-",[177,2724,2725],{"class":230}," 1",[177,2727,2728],{"class":187},", delay ",[177,2730,2608],{"class":183},[177,2732,2733],{"class":230}," 2",[177,2735,2736],{"class":187},", init);\n",[177,2738,2739],{"class":179,"line":647},[177,2740,967],{"class":187},[177,2742,2743],{"class":179,"line":670},[177,2744,644],{"class":187},[177,2746,2747,2749],{"class":179,"line":701},[177,2748,728],{"class":183},[177,2750,2751],{"class":187}," res;\n",[177,2753,2754,2756,2758],{"class":179,"line":720},[177,2755,741],{"class":187},[177,2757,744],{"class":183},[177,2759,747],{"class":187},[177,2761,2762,2764,2767,2769,2771],{"class":179,"line":725},[177,2763,932],{"class":183},[177,2765,2766],{"class":187}," (retries ",[177,2768,2578],{"class":183},[177,2770,987],{"class":230},[177,2772,1200],{"class":187},[177,2774,2775,2777,2779,2781,2784,2786,2789],{"class":179,"line":738},[177,2776,2644],{"class":187},[177,2778,1192],{"class":237},[177,2780,241],{"class":187},[177,2782,2783],{"class":194},"`[Network Error] ${",[177,2785,2497],{"class":187},[177,2787,2788],{"class":194},"}. Retrying...`",[177,2790,588],{"class":187},[177,2792,2793,2795,2797,2799,2801,2803,2805,2807,2809],{"class":179,"line":750},[177,2794,2712],{"class":183},[177,2796,2492],{"class":237},[177,2798,2719],{"class":187},[177,2800,2722],{"class":183},[177,2802,2725],{"class":230},[177,2804,2728],{"class":187},[177,2806,2608],{"class":183},[177,2808,2733],{"class":230},[177,2810,2736],{"class":187},[177,2812,2813],{"class":179,"line":787},[177,2814,967],{"class":187},[177,2816,2817,2819],{"class":179,"line":793},[177,2818,753],{"class":183},[177,2820,2821],{"class":187}," err;\n",[177,2823,2824],{"class":179,"line":798},[177,2825,790],{"class":187},[177,2827,2828],{"class":179,"line":803},[177,2829,1536],{"class":187},[17,2831,2832,2833,2836],{},"Next, in the mass operation functions themselves (like massBuy, massSell, and massOut), instead of executing every wallet transaction simultaneously with a giant ",[174,2834,2835],{},"Promise.all",", I chunked the processes. By grouping the wallets into batches and adding a sleep delay between them, it gives the public API time to breathe.",[17,2838,2839,2840,530],{},"Here is a snippet of how that processing loop looks inside ",[174,2841,2842],{},"massBuy.ts",[167,2844,2846],{"className":169,"code":2845,"language":172,"meta":15,"style":15},"await chunkProcess(walletData, chunksToProcess, async (chunk) => {\n        await Promise.all(\n            chunk.map(async ({balance, wallet}) => {\n\n                // balance checks and URL setup \n\n                const orderResponse = await fetchWithRetry(String(orderUrl), 5, 500, { \n                    headers: { \n                        \"x-api-key\": API_KEY \n                    } \n                });\n\n                // transaction signing \n\n              const executeResponse = await fetchWithRetry(`${JUPITER_BASE_URL}/execute`, 5, 500, {\n                    method: \"POST\",\n                    headers: {\n                        \"Content-Type\": \"application/json\",\n                        \"x-api-key\": API_KEY,\n                    },\n                    body: JSON.stringify({\n                        signedTransaction: Buffer.from(transaction.serialize()).toString(\"base64\"),\n                        requestId: orderResult.requestId,\n                    }),\n                });\n\n            })\n        );\n         // Sleep after each chunk to avoid hitting rate limits on the next batch\n         await sleep(1000);\n    });\n\n",[174,2847,2848,2873,2887,2917,2921,2927,2931,2964,2969,2983,2988,2993,2997,3002,3006,3041,3051,3056,3068,3078,3083,3098,3124,3129,3134,3138,3142,3147,3152,3157,3172],{"__ignoreMap":15},[177,2849,2850,2853,2856,2859,2862,2864,2867,2869,2871],{"class":179,"line":180},[177,2851,2852],{"class":183},"await",[177,2854,2855],{"class":237}," chunkProcess",[177,2857,2858],{"class":187},"(walletData, chunksToProcess, ",[177,2860,2861],{"class":183},"async",[177,2863,523],{"class":187},[177,2865,2866],{"class":526},"chunk",[177,2868,1905],{"class":187},[177,2870,1908],{"class":183},[177,2872,453],{"class":187},[177,2874,2875,2877,2879,2881,2884],{"class":179,"line":25},[177,2876,2084],{"class":183},[177,2878,2694],{"class":230},[177,2880,65],{"class":187},[177,2882,2883],{"class":237},"all",[177,2885,2886],{"class":187},"(\n",[177,2888,2889,2892,2895,2897,2899,2902,2905,2907,2910,2913,2915],{"class":179,"line":215},[177,2890,2891],{"class":187},"            chunk.",[177,2893,2894],{"class":237},"map",[177,2896,241],{"class":187},[177,2898,2861],{"class":183},[177,2900,2901],{"class":187}," ({",[177,2903,2904],{"class":526},"balance",[177,2906,536],{"class":187},[177,2908,2909],{"class":526},"wallet",[177,2911,2912],{"class":187},"}) ",[177,2914,1908],{"class":183},[177,2916,453],{"class":187},[177,2918,2919],{"class":179,"line":221},[177,2920,218],{"emptyLinePlaceholder":30},[177,2922,2923],{"class":179,"line":250},[177,2924,2926],{"class":2925},"sCsY4","                // balance checks and URL setup \n",[177,2928,2929],{"class":179,"line":285},[177,2930,218],{"emptyLinePlaceholder":30},[177,2932,2933,2936,2939,2941,2943,2945,2947,2950,2953,2956,2958,2961],{"class":179,"line":308},[177,2934,2935],{"class":183},"                const",[177,2937,2938],{"class":230}," orderResponse",[177,2940,234],{"class":183},[177,2942,1600],{"class":183},[177,2944,2492],{"class":237},[177,2946,241],{"class":187},[177,2948,2949],{"class":237},"String",[177,2951,2952],{"class":187},"(orderUrl), ",[177,2954,2955],{"class":230},"5",[177,2957,536],{"class":187},[177,2959,2960],{"class":230},"500",[177,2962,2963],{"class":187},", { \n",[177,2965,2966],{"class":179,"line":329},[177,2967,2968],{"class":187},"                    headers: { \n",[177,2970,2971,2974,2977,2980],{"class":179,"line":350},[177,2972,2973],{"class":194},"                        \"x-api-key\"",[177,2975,2976],{"class":187},": ",[177,2978,2979],{"class":230},"API_KEY",[177,2981,2982],{"class":187}," \n",[177,2984,2985],{"class":179,"line":384},[177,2986,2987],{"class":187},"                    } \n",[177,2989,2990],{"class":179,"line":591},[177,2991,2992],{"class":187},"                });\n",[177,2994,2995],{"class":179,"line":613},[177,2996,218],{"emptyLinePlaceholder":30},[177,2998,2999],{"class":179,"line":641},[177,3000,3001],{"class":2925},"                // transaction signing \n",[177,3003,3004],{"class":179,"line":647},[177,3005,218],{"emptyLinePlaceholder":30},[177,3007,3008,3011,3014,3016,3018,3020,3022,3025,3028,3031,3033,3035,3037,3039],{"class":179,"line":670},[177,3009,3010],{"class":183},"              const",[177,3012,3013],{"class":230}," executeResponse",[177,3015,234],{"class":183},[177,3017,1600],{"class":183},[177,3019,2492],{"class":237},[177,3021,241],{"class":187},[177,3023,3024],{"class":194},"`${",[177,3026,3027],{"class":230},"JUPITER_BASE_URL",[177,3029,3030],{"class":194},"}/execute`",[177,3032,536],{"class":187},[177,3034,2955],{"class":230},[177,3036,536],{"class":187},[177,3038,2960],{"class":230},[177,3040,247],{"class":187},[177,3042,3043,3046,3049],{"class":179,"line":701},[177,3044,3045],{"class":187},"                    method: ",[177,3047,3048],{"class":194},"\"POST\"",[177,3050,464],{"class":187},[177,3052,3053],{"class":179,"line":720},[177,3054,3055],{"class":187},"                    headers: {\n",[177,3057,3058,3061,3063,3066],{"class":179,"line":725},[177,3059,3060],{"class":194},"                        \"Content-Type\"",[177,3062,2976],{"class":187},[177,3064,3065],{"class":194},"\"application/json\"",[177,3067,464],{"class":187},[177,3069,3070,3072,3074,3076],{"class":179,"line":738},[177,3071,2973],{"class":194},[177,3073,2976],{"class":187},[177,3075,2979],{"class":230},[177,3077,464],{"class":187},[177,3079,3080],{"class":179,"line":750},[177,3081,3082],{"class":187},"                    },\n",[177,3084,3085,3088,3091,3093,3096],{"class":179,"line":787},[177,3086,3087],{"class":187},"                    body: ",[177,3089,3090],{"class":230},"JSON",[177,3092,65],{"class":187},[177,3094,3095],{"class":237},"stringify",[177,3097,1478],{"class":187},[177,3099,3100,3103,3105,3108,3111,3114,3117,3119,3122],{"class":179,"line":793},[177,3101,3102],{"class":187},"                        signedTransaction: Buffer.",[177,3104,191],{"class":237},[177,3106,3107],{"class":187},"(transaction.",[177,3109,3110],{"class":237},"serialize",[177,3112,3113],{"class":187},"()).",[177,3115,3116],{"class":237},"toString",[177,3118,241],{"class":187},[177,3120,3121],{"class":194},"\"base64\"",[177,3123,381],{"class":187},[177,3125,3126],{"class":179,"line":798},[177,3127,3128],{"class":187},"                        requestId: orderResult.requestId,\n",[177,3130,3131],{"class":179,"line":803},[177,3132,3133],{"class":187},"                    }),\n",[177,3135,3136],{"class":179,"line":842},[177,3137,2992],{"class":187},[177,3139,3140],{"class":179,"line":849},[177,3141,218],{"emptyLinePlaceholder":30},[177,3143,3144],{"class":179,"line":854},[177,3145,3146],{"class":187},"            })\n",[177,3148,3149],{"class":179,"line":869},[177,3150,3151],{"class":187},"        );\n",[177,3153,3154],{"class":179,"line":885},[177,3155,3156],{"class":2925},"         // Sleep after each chunk to avoid hitting rate limits on the next batch\n",[177,3158,3159,3162,3165,3167,3170],{"class":179,"line":900},[177,3160,3161],{"class":183},"         await",[177,3163,3164],{"class":237}," sleep",[177,3166,241],{"class":187},[177,3168,3169],{"class":230},"1000",[177,3171,588],{"class":187},[177,3173,3174],{"class":179,"line":924},[177,3175,1506],{"class":187},[17,3177,3178],{},"This combination of chunking the workload and adding retry logic lets the bot handle concurrent mass operations safely and reliably, even when relying entirely on public nodes.",[3180,3181,3182],"note",{},[17,3183,2432,3184,3191],{},[53,3185,3188],{"href":3186,"rel":3187},"https://docs.riavzon.com/docs/utils/shared/chunkprocess",[57],[174,3189,3190],{},"chunkProcess"," function comes also from my utils package",[17,3193,3194,3195,3198],{},"But EVEN THAT was not enough for the sell functions (massOut, and massSell) to succeed. Because at first the logic in these functions was heavily using the rpc's methods, then refactored to make a single call with the ",[174,3196,3197],{},"getProgramAccounts"," method to get all accounts and parse it locally (find the wallet, and the target mint), which also didn't work (rate limited), and was error prone.",[17,3200,3201],{},"So I implemented a persistent positions tracker, with the following schema:",[167,3203,3206],{"className":169,"code":3204,"filename":3205,"language":172,"meta":15,"style":15},"import { sql } from 'drizzle-orm';\nimport { sqliteTable, text, integer, numeric } from 'drizzle-orm/sqlite-core';\nimport { walletTable } from './wallets';\n\n\n\nexport const positionsTable = sqliteTable('positions', {\n  id: integer('id').primaryKey({ autoIncrement: true }),\n  walletAddress: text('wallet_address').notNull().references(() => walletTable.publicAddress, { onDelete: 'cascade' }),\n  targetMintAddress: text('target_mint_address').notNull(),\n  solAmounts: numeric('sol_amounts', { mode: 'bigint' }),\n  tokenAmount: numeric('token_amount', { mode: 'bigint' }),\n  walletRemainingBalance: numeric(\"wallet_remaining_balance\", { mode: 'bigint' }),\n  updatedAt: integer('updated_at', {mode: 'timestamp'}).default(sql`(cast(unixepoch() as int))`),\n});\n","buyPositions.ts",[174,3207,3208,3221,3235,3249,3253,3257,3261,3281,3302,3336,3354,3375,3393,3411,3440],{"__ignoreMap":15},[177,3209,3210,3212,3214,3216,3219],{"class":179,"line":180},[177,3211,184],{"class":183},[177,3213,205],{"class":187},[177,3215,191],{"class":183},[177,3217,3218],{"class":194}," 'drizzle-orm'",[177,3220,198],{"class":187},[177,3222,3223,3225,3228,3230,3233],{"class":179,"line":25},[177,3224,184],{"class":183},[177,3226,3227],{"class":187}," { sqliteTable, text, integer, numeric } ",[177,3229,191],{"class":183},[177,3231,3232],{"class":194}," 'drizzle-orm/sqlite-core'",[177,3234,198],{"class":187},[177,3236,3237,3239,3242,3244,3247],{"class":179,"line":215},[177,3238,184],{"class":183},[177,3240,3241],{"class":187}," { walletTable } ",[177,3243,191],{"class":183},[177,3245,3246],{"class":194}," './wallets'",[177,3248,198],{"class":187},[177,3250,3251],{"class":179,"line":221},[177,3252,218],{"emptyLinePlaceholder":30},[177,3254,3255],{"class":179,"line":250},[177,3256,218],{"emptyLinePlaceholder":30},[177,3258,3259],{"class":179,"line":285},[177,3260,218],{"emptyLinePlaceholder":30},[177,3262,3263,3265,3267,3270,3272,3274,3276,3279],{"class":179,"line":308},[177,3264,224],{"class":183},[177,3266,227],{"class":183},[177,3268,3269],{"class":230}," positionsTable",[177,3271,234],{"class":183},[177,3273,238],{"class":237},[177,3275,241],{"class":187},[177,3277,3278],{"class":194},"'positions'",[177,3280,247],{"class":187},[177,3282,3283,3286,3288,3290,3292,3294,3296,3298,3300],{"class":179,"line":329},[177,3284,3285],{"class":187},"  id: ",[177,3287,256],{"class":237},[177,3289,241],{"class":187},[177,3291,261],{"class":194},[177,3293,299],{"class":187},[177,3295,273],{"class":237},[177,3297,276],{"class":187},[177,3299,279],{"class":230},[177,3301,282],{"class":187},[177,3303,3304,3307,3309,3311,3314,3316,3318,3320,3323,3326,3328,3331,3334],{"class":179,"line":350},[177,3305,3306],{"class":187},"  walletAddress: ",[177,3308,291],{"class":237},[177,3310,241],{"class":187},[177,3312,3313],{"class":194},"'wallet_address'",[177,3315,299],{"class":187},[177,3317,345],{"class":237},[177,3319,367],{"class":187},[177,3321,3322],{"class":237},"references",[177,3324,3325],{"class":187},"(() ",[177,3327,1908],{"class":183},[177,3329,3330],{"class":187}," walletTable.publicAddress, { onDelete: ",[177,3332,3333],{"class":194},"'cascade'",[177,3335,282],{"class":187},[177,3337,3338,3341,3343,3345,3348,3350,3352],{"class":179,"line":384},[177,3339,3340],{"class":187},"  targetMintAddress: ",[177,3342,291],{"class":237},[177,3344,241],{"class":187},[177,3346,3347],{"class":194},"'target_mint_address'",[177,3349,299],{"class":187},[177,3351,345],{"class":237},[177,3353,305],{"class":187},[177,3355,3356,3359,3362,3364,3367,3370,3373],{"class":179,"line":591},[177,3357,3358],{"class":187},"  solAmounts: ",[177,3360,3361],{"class":237},"numeric",[177,3363,241],{"class":187},[177,3365,3366],{"class":194},"'sol_amounts'",[177,3368,3369],{"class":187},", { mode: ",[177,3371,3372],{"class":194},"'bigint'",[177,3374,282],{"class":187},[177,3376,3377,3380,3382,3384,3387,3389,3391],{"class":179,"line":613},[177,3378,3379],{"class":187},"  tokenAmount: ",[177,3381,3361],{"class":237},[177,3383,241],{"class":187},[177,3385,3386],{"class":194},"'token_amount'",[177,3388,3369],{"class":187},[177,3390,3372],{"class":194},[177,3392,282],{"class":187},[177,3394,3395,3398,3400,3402,3405,3407,3409],{"class":179,"line":641},[177,3396,3397],{"class":187},"  walletRemainingBalance: ",[177,3399,3361],{"class":237},[177,3401,241],{"class":187},[177,3403,3404],{"class":194},"\"wallet_remaining_balance\"",[177,3406,3369],{"class":187},[177,3408,3372],{"class":194},[177,3410,282],{"class":187},[177,3412,3413,3416,3418,3420,3423,3425,3428,3430,3432,3434,3436,3438],{"class":179,"line":647},[177,3414,3415],{"class":187},"  updatedAt: ",[177,3417,256],{"class":237},[177,3419,241],{"class":187},[177,3421,3422],{"class":194},"'updated_at'",[177,3424,264],{"class":187},[177,3426,3427],{"class":194},"'timestamp'",[177,3429,270],{"class":187},[177,3431,370],{"class":237},[177,3433,241],{"class":187},[177,3435,375],{"class":237},[177,3437,378],{"class":194},[177,3439,381],{"class":187},[177,3441,3442],{"class":179,"line":670},[177,3443,387],{"class":187},[17,3445,3446],{},"This schema completely eliminated the need to ask the RPC node, \"what tokens do these 50 wallets hold?\" Instead of relying on the network for state, the application tracks it locally.",[17,3448,3449,3450,3453],{},"Every time a ",[174,3451,3452],{},"massBuy"," operation succeeds on-chain, it immediately records the exact resulting position, down to the specific token amount received, into this local database:",[167,3455,3457],{"className":169,"code":3456,"filename":2842,"language":172,"meta":15,"style":15},"// inside the execution loop\nif (result.status === \"Success\") {\n    log.success(`Buy Success! https://solscan.io/tx/${result.signature}`);\n    try {\n        await db.insert(positionsTable).values({\n            walletAddress: wallet.publicKey.toBase58(),\n            targetMintAddress: targetMintAddress,\n            solAmounts: lamportsPerWallet,\n            walletRemainingBalance: BigInt(balance),\n            tokenAmount: BigInt(result.outputAmountResult), // The exact amount received\n        });\n    } catch (err) { \n        log.error(\"Database Error (Swap succeeded on-chain, but failed to record)\", err);\n    }\n}\n\n",[174,3458,3459,3464,3479,3503,3509,3524,3533,3538,3543,3554,3567,3571,3580,3595,3599],{"__ignoreMap":15},[177,3460,3461],{"class":179,"line":180},[177,3462,3463],{"class":2925},"// inside the execution loop\n",[177,3465,3466,3469,3472,3474,3477],{"class":179,"line":25},[177,3467,3468],{"class":183},"if",[177,3470,3471],{"class":187}," (result.status ",[177,3473,1268],{"class":183},[177,3475,3476],{"class":194}," \"Success\"",[177,3478,1200],{"class":187},[177,3480,3481,3484,3486,3488,3491,3494,3496,3499,3501],{"class":179,"line":215},[177,3482,3483],{"class":187},"    log.",[177,3485,1517],{"class":237},[177,3487,241],{"class":187},[177,3489,3490],{"class":194},"`Buy Success! https://solscan.io/tx/${",[177,3492,3493],{"class":187},"result",[177,3495,65],{"class":194},[177,3497,3498],{"class":187},"signature",[177,3500,782],{"class":194},[177,3502,588],{"class":187},[177,3504,3505,3507],{"class":179,"line":221},[177,3506,1792],{"class":183},[177,3508,453],{"class":187},[177,3510,3511,3513,3515,3517,3520,3522],{"class":179,"line":250},[177,3512,2084],{"class":183},[177,3514,1466],{"class":187},[177,3516,1469],{"class":237},[177,3518,3519],{"class":187},"(positionsTable).",[177,3521,1475],{"class":237},[177,3523,1478],{"class":187},[177,3525,3526,3529,3531],{"class":179,"line":285},[177,3527,3528],{"class":187},"            walletAddress: wallet.publicKey.",[177,3530,1486],{"class":237},[177,3532,305],{"class":187},[177,3534,3535],{"class":179,"line":308},[177,3536,3537],{"class":187},"            targetMintAddress: targetMintAddress,\n",[177,3539,3540],{"class":179,"line":329},[177,3541,3542],{"class":187},"            solAmounts: lamportsPerWallet,\n",[177,3544,3545,3548,3551],{"class":179,"line":350},[177,3546,3547],{"class":187},"            walletRemainingBalance: ",[177,3549,3550],{"class":237},"BigInt",[177,3552,3553],{"class":187},"(balance),\n",[177,3555,3556,3559,3561,3564],{"class":179,"line":384},[177,3557,3558],{"class":187},"            tokenAmount: ",[177,3560,3550],{"class":237},[177,3562,3563],{"class":187},"(result.outputAmountResult), ",[177,3565,3566],{"class":2925},"// The exact amount received\n",[177,3568,3569],{"class":179,"line":591},[177,3570,2121],{"class":187},[177,3572,3573,3575,3577],{"class":179,"line":613},[177,3574,2156],{"class":187},[177,3576,744],{"class":183},[177,3578,3579],{"class":187}," (err) { \n",[177,3581,3582,3585,3587,3589,3592],{"class":179,"line":641},[177,3583,3584],{"class":187},"        log.",[177,3586,1192],{"class":237},[177,3588,241],{"class":187},[177,3590,3591],{"class":194},"\"Database Error (Swap succeeded on-chain, but failed to record)\"",[177,3593,3594],{"class":187},", err);\n",[177,3596,3597],{"class":179,"line":647},[177,3598,967],{"class":187},[177,3600,3601],{"class":179,"line":670},[177,3602,1536],{"class":187},[17,3604,3605,3606,3609,3610,3613],{},"Then, when it is time to trigger a ",[174,3607,3608],{},"massSell"," or a panic ",[174,3611,3612],{},"massOut",", the bot skips the heavy network queries entirely. It simply reads the active positions directly from SQLite, matches them with the correct encrypted private keys, and goes straight to building the sell orders:",[167,3615,3618],{"className":169,"code":3616,"filename":3617,"language":172,"meta":15,"style":15},"const filters: SQL[] = [];\n  filters.push(eq(positionsTable.targetMintAddress, targetMintAddress));\n\n  // optional filtering logic if specific wallets were chosen\n\n  const limit = (chosenWallets && chosenWallets.length > 0) ? chosenWallets.length : numberOfWallets;\n  const positions = await db.select().from(positionsTable).where(and(...filters)).limit(limit);\n\n  if (positions.length === 0) {\n    log.warn(`No positions found in database for mint: ${targetMintAddress}`);\n    return { \n        ok: false, \n        reason: `No positions found in database for mint: ${targetMintAddress}`\n    };\n  }\n\n","sell.ts",[174,3619,3620,3639,3654,3658,3663,3667,3707,3749,3753,3770,3788,3795,3806,3818,3823],{"__ignoreMap":15},[177,3621,3622,3624,3627,3629,3632,3634,3636],{"class":179,"line":180},[177,3623,445],{"class":183},[177,3625,3626],{"class":230}," filters",[177,3628,530],{"class":183},[177,3630,3631],{"class":237}," SQL",[177,3633,1866],{"class":187},[177,3635,984],{"class":183},[177,3637,3638],{"class":187}," [];\n",[177,3640,3641,3644,3647,3649,3651],{"class":179,"line":25},[177,3642,3643],{"class":187},"  filters.",[177,3645,3646],{"class":237},"push",[177,3648,241],{"class":187},[177,3650,1619],{"class":237},[177,3652,3653],{"class":187},"(positionsTable.targetMintAddress, targetMintAddress));\n",[177,3655,3656],{"class":179,"line":215},[177,3657,218],{"emptyLinePlaceholder":30},[177,3659,3660],{"class":179,"line":221},[177,3661,3662],{"class":2925},"  // optional filtering logic if specific wallets were chosen\n",[177,3664,3665],{"class":179,"line":250},[177,3666,218],{"emptyLinePlaceholder":30},[177,3668,3669,3672,3675,3677,3680,3682,3685,3687,3690,3692,3694,3697,3699,3701,3704],{"class":179,"line":285},[177,3670,3671],{"class":183},"  const",[177,3673,3674],{"class":230}," limit",[177,3676,234],{"class":183},[177,3678,3679],{"class":187}," (chosenWallets ",[177,3681,1890],{"class":183},[177,3683,3684],{"class":187}," chosenWallets.",[177,3686,938],{"class":230},[177,3688,3689],{"class":183}," >",[177,3691,987],{"class":230},[177,3693,1905],{"class":187},[177,3695,3696],{"class":183},"?",[177,3698,3684],{"class":187},[177,3700,938],{"class":230},[177,3702,3703],{"class":183}," :",[177,3705,3706],{"class":187}," numberOfWallets;\n",[177,3708,3709,3711,3714,3716,3718,3720,3722,3724,3726,3728,3730,3732,3735,3737,3740,3743,3746],{"class":179,"line":308},[177,3710,3671],{"class":183},[177,3712,3713],{"class":230}," positions",[177,3715,234],{"class":183},[177,3717,1600],{"class":183},[177,3719,1466],{"class":187},[177,3721,1605],{"class":237},[177,3723,367],{"class":187},[177,3725,191],{"class":237},[177,3727,3519],{"class":187},[177,3729,1614],{"class":237},[177,3731,241],{"class":187},[177,3733,3734],{"class":237},"and",[177,3736,241],{"class":187},[177,3738,3739],{"class":183},"...",[177,3741,3742],{"class":187},"filters)).",[177,3744,3745],{"class":237},"limit",[177,3747,3748],{"class":187},"(limit);\n",[177,3750,3751],{"class":179,"line":329},[177,3752,218],{"emptyLinePlaceholder":30},[177,3754,3755,3758,3761,3763,3766,3768],{"class":179,"line":350},[177,3756,3757],{"class":183},"  if",[177,3759,3760],{"class":187}," (positions.",[177,3762,938],{"class":230},[177,3764,3765],{"class":183}," ===",[177,3767,987],{"class":230},[177,3769,1200],{"class":187},[177,3771,3772,3774,3776,3778,3781,3784,3786],{"class":179,"line":384},[177,3773,3483],{"class":187},[177,3775,2647],{"class":237},[177,3777,241],{"class":187},[177,3779,3780],{"class":194},"`No positions found in database for mint: ${",[177,3782,3783],{"class":187},"targetMintAddress",[177,3785,782],{"class":194},[177,3787,588],{"class":187},[177,3789,3790,3792],{"class":179,"line":591},[177,3791,728],{"class":183},[177,3793,3794],{"class":187}," { \n",[177,3796,3797,3800,3803],{"class":179,"line":613},[177,3798,3799],{"class":187},"        ok: ",[177,3801,3802],{"class":230},"false",[177,3804,3805],{"class":187},", \n",[177,3807,3808,3811,3813,3815],{"class":179,"line":641},[177,3809,3810],{"class":187},"        reason: ",[177,3812,3780],{"class":194},[177,3814,3783],{"class":187},[177,3816,3817],{"class":194},"}`\n",[177,3819,3820],{"class":179,"line":647},[177,3821,3822],{"class":187},"    };\n",[177,3824,3825],{"class":179,"line":670},[177,3826,790],{"class":187},[17,3828,3829,3830,1543,3833,3835],{},"Adding this local state lets the sell operations instantly know exactly which wallets hold the target mint and exactly how much they hold. This drastically reduced the total number of RPC requests, completely bypassing the rate-limit errors I was hitting with ",[174,3831,3832],{},"getParsedTokenAccountsByOwner",[174,3834,3197],{}," before even reaching the Jupiter API.",[17,3837,3838],{},"If a sell succeeds, the local database simply deletes that position row (or updates the balance if it was a partial sell percentage).",[17,3840,3841,3842,536,3844,536,3846,3848],{},"With the core already figured out, and tested locally with a small amount of SOL in a live environment, it was time to add that Volume bot. This part actually wasn't that challenging after those three core functions (",[174,3843,3452],{},[174,3845,3608],{},[174,3847,3612],{},") were in place. A volume bot simply needs to reuse these three functions in a continuous loop to generate trading activity.",[17,3850,3851],{},"The main challenge here shifted from blockchain logic to process management. I needed the volume bot to run continuously in the background, independent of the main CLI, without locking up the user's terminal.",[17,3853,3854,3855,3858],{},"To achieve this, I used Node's ",[174,3856,3857],{},"child_process.spawn"," to start a detached background worker. To keep things simple and avoid complex IPC (Inter-Process Communication) at this stage, I passed the configuration down to the worker via environment variables:",[167,3860,3863],{"className":169,"code":3861,"filename":3862,"language":172,"meta":15,"style":15},"import { spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\n\nconst child = spawn(process.execPath, process.argv.slice(1), {\n    detached: true,\n    stdio: \"ignore\",\n    env: {\n        ...process.env,\n        SOLANA_BOTS_INTERNAL: \"volume-worker\",\n        TARGET_MINT: args.target,\n        WALLETS: args.wallets,\n        AMOUNT: args.amount,\n        PASSWORD: args.password,\n        INTERVAL: args.interval,\n        TTL: args.ttl ?? \"\",\n    },\n});\n\nchild.unref();\n\nawait fs.writeFile(PID_FILE, String(child.pid));\nconsola.success(`Volume bot started in background. PID: ${child.pid}`);\n\n","main.ts",[174,3864,3865,3879,3893,3897,3922,3931,3941,3946,3954,3964,3969,3974,3979,3984,3989,4002,4007,4011,4015,4025,4029,4050],{"__ignoreMap":15},[177,3866,3867,3869,3872,3874,3877],{"class":179,"line":180},[177,3868,184],{"class":183},[177,3870,3871],{"class":187}," { spawn } ",[177,3873,191],{"class":183},[177,3875,3876],{"class":194}," 'node:child_process'",[177,3878,198],{"class":187},[177,3880,3881,3883,3886,3888,3891],{"class":179,"line":25},[177,3882,184],{"class":183},[177,3884,3885],{"class":187}," fs ",[177,3887,191],{"class":183},[177,3889,3890],{"class":194}," 'node:fs/promises'",[177,3892,198],{"class":187},[177,3894,3895],{"class":179,"line":215},[177,3896,218],{"emptyLinePlaceholder":30},[177,3898,3899,3901,3904,3906,3909,3912,3915,3917,3919],{"class":179,"line":221},[177,3900,445],{"class":183},[177,3902,3903],{"class":230}," child",[177,3905,234],{"class":183},[177,3907,3908],{"class":237}," spawn",[177,3910,3911],{"class":187},"(process.execPath, process.argv.",[177,3913,3914],{"class":237},"slice",[177,3916,241],{"class":187},[177,3918,493],{"class":230},[177,3920,3921],{"class":187},"), {\n",[177,3923,3924,3927,3929],{"class":179,"line":250},[177,3925,3926],{"class":187},"    detached: ",[177,3928,279],{"class":230},[177,3930,464],{"class":187},[177,3932,3933,3936,3939],{"class":179,"line":285},[177,3934,3935],{"class":187},"    stdio: ",[177,3937,3938],{"class":194},"\"ignore\"",[177,3940,464],{"class":187},[177,3942,3943],{"class":179,"line":308},[177,3944,3945],{"class":187},"    env: {\n",[177,3947,3948,3951],{"class":179,"line":329},[177,3949,3950],{"class":183},"        ...",[177,3952,3953],{"class":187},"process.env,\n",[177,3955,3956,3959,3962],{"class":179,"line":350},[177,3957,3958],{"class":187},"        SOLANA_BOTS_INTERNAL: ",[177,3960,3961],{"class":194},"\"volume-worker\"",[177,3963,464],{"class":187},[177,3965,3966],{"class":179,"line":384},[177,3967,3968],{"class":187},"        TARGET_MINT: args.target,\n",[177,3970,3971],{"class":179,"line":591},[177,3972,3973],{"class":187},"        WALLETS: args.wallets,\n",[177,3975,3976],{"class":179,"line":613},[177,3977,3978],{"class":187},"        AMOUNT: args.amount,\n",[177,3980,3981],{"class":179,"line":641},[177,3982,3983],{"class":187},"        PASSWORD: args.password,\n",[177,3985,3986],{"class":179,"line":647},[177,3987,3988],{"class":187},"        INTERVAL: args.interval,\n",[177,3990,3991,3994,3997,4000],{"class":179,"line":670},[177,3992,3993],{"class":187},"        TTL: args.ttl ",[177,3995,3996],{"class":183},"??",[177,3998,3999],{"class":194}," \"\"",[177,4001,464],{"class":187},[177,4003,4004],{"class":179,"line":701},[177,4005,4006],{"class":187},"    },\n",[177,4008,4009],{"class":179,"line":720},[177,4010,387],{"class":187},[177,4012,4013],{"class":179,"line":725},[177,4014,218],{"emptyLinePlaceholder":30},[177,4016,4017,4020,4023],{"class":179,"line":738},[177,4018,4019],{"class":187},"child.",[177,4021,4022],{"class":237},"unref",[177,4024,717],{"class":187},[177,4026,4027],{"class":179,"line":750},[177,4028,218],{"emptyLinePlaceholder":30},[177,4030,4031,4033,4035,4038,4040,4043,4045,4047],{"class":179,"line":787},[177,4032,2852],{"class":183},[177,4034,1717],{"class":187},[177,4036,4037],{"class":237},"writeFile",[177,4039,241],{"class":187},[177,4041,4042],{"class":230},"PID_FILE",[177,4044,536],{"class":187},[177,4046,2949],{"class":237},[177,4048,4049],{"class":187},"(child.pid));\n",[177,4051,4052,4055,4057,4059,4062,4065,4067,4070,4072],{"class":179,"line":793},[177,4053,4054],{"class":187},"consola.",[177,4056,1517],{"class":237},[177,4058,241],{"class":187},[177,4060,4061],{"class":194},"`Volume bot started in background. PID: ${",[177,4063,4064],{"class":187},"child",[177,4066,65],{"class":194},[177,4068,4069],{"class":187},"pid",[177,4071,782],{"class":194},[177,4073,588],{"class":187},[17,4075,4076,4077,4080,4081,4084],{},"By setting ",[174,4078,4079],{},"detached: true"," and calling ",[174,4082,4083],{},"child.unref()",", the worker detaches from the parent terminal. I then save its Process ID to a local .pid file so the CLI can keep track of it and shut it down later.",[17,4086,4087],{},"Inside the worker process itself, it runs a continuous while loop until a specified TTL expires or it receives a stop signal. But I couldn't just grab the same wallets every single loop, if a user has 50 wallets but only wants to run volume with 5 at a time, the bot should cycle through them to distribute the trading activity naturally.",[17,4089,4090,4091,4094],{},"To solve this, I implemented a round-robin rotation logic using a ",[174,4092,4093],{},"walletOffset"," index. In each cycle, it grabs a pool of eligible wallets (wallets that actually have enough SOL to cover the trade amount plus the network fees), and then slices out the requested batch:",[167,4096,4099],{"className":169,"code":4097,"filename":4098,"language":172,"meta":15,"style":15},"let walletOffset = 0;\n\nwhile (!stopping && Date.now() \u003C expiresAt) {\n    // Fetch all wallets that can afford the trade + fees\n    const pool = await getEligibleWallets(password, maxSolPerWallet + NETWORK_FEE_BUFFER);\n    \n    if (pool.length \u003C numberOfWallets) {\n        consola.error(`Only ${pool.length} wallets have enough SOL. Need ${numberOfWallets}.`);\n        await sleep(5000);\n        continue; \n    }\n\n    // rotation logic\n    const selectedWallets = [];\n    for (let i = 0; i \u003C numberOfWallets; i++) {\n        const index = (walletOffset + i) % pool.length;\n        selectedWallets.push(pool[index]);\n    }\n    \n    // Shift offset\n    walletOffset = (walletOffset + numberOfWallets) % pool.length;\n\n    const selectedKeypairs = selectedWallets.map(entry => entry.wallet);\n\n    try {\n        // 3. Execute Mass Buy\n        const buyResult = await massBuy(targetMintAddress, maxSolPerWallet, numberOfWallets, password, 1, selectedKeypairs);\n\n    } catch (err) {\n\n    }\n\n    // Mass Sell\n\n    await sleep(interval ?? 0);\n}\n","volumeBotWorker.ts",[174,4100,4101,4115,4119,4147,4152,4176,4180,4194,4223,4236,4243,4247,4251,4256,4267,4296,4323,4333,4337,4341,4346,4368,4372,4396,4400,4406,4411,4433,4437,4445,4449,4453,4457,4462,4466,4482],{"__ignoreMap":15},[177,4102,4103,4106,4109,4111,4113],{"class":179,"line":180},[177,4104,4105],{"class":183},"let",[177,4107,4108],{"class":187}," walletOffset ",[177,4110,984],{"class":183},[177,4112,987],{"class":230},[177,4114,198],{"class":187},[177,4116,4117],{"class":179,"line":25},[177,4118,218],{"emptyLinePlaceholder":30},[177,4120,4121,4124,4126,4128,4131,4133,4136,4139,4141,4144],{"class":179,"line":215},[177,4122,4123],{"class":183},"while",[177,4125,523],{"class":187},[177,4127,1741],{"class":183},[177,4129,4130],{"class":187},"stopping ",[177,4132,1890],{"class":183},[177,4134,4135],{"class":187}," Date.",[177,4137,4138],{"class":237},"now",[177,4140,2605],{"class":187},[177,4142,4143],{"class":183},"\u003C",[177,4145,4146],{"class":187}," expiresAt) {\n",[177,4148,4149],{"class":179,"line":221},[177,4150,4151],{"class":2925},"    // Fetch all wallets that can afford the trade + fees\n",[177,4153,4154,4156,4159,4161,4163,4166,4169,4171,4174],{"class":179,"line":250},[177,4155,569],{"class":183},[177,4157,4158],{"class":230}," pool",[177,4160,234],{"class":183},[177,4162,1600],{"class":183},[177,4164,4165],{"class":237}," getEligibleWallets",[177,4167,4168],{"class":187},"(password, maxSolPerWallet ",[177,4170,913],{"class":183},[177,4172,4173],{"class":230}," NETWORK_FEE_BUFFER",[177,4175,588],{"class":187},[177,4177,4178],{"class":179,"line":285},[177,4179,644],{"class":187},[177,4181,4182,4184,4187,4189,4191],{"class":179,"line":308},[177,4183,932],{"class":183},[177,4185,4186],{"class":187}," (pool.",[177,4188,938],{"class":230},[177,4190,941],{"class":183},[177,4192,4193],{"class":187}," numberOfWallets) {\n",[177,4195,4196,4198,4200,4202,4205,4208,4210,4212,4215,4218,4221],{"class":179,"line":329},[177,4197,1649],{"class":187},[177,4199,1192],{"class":237},[177,4201,241],{"class":187},[177,4203,4204],{"class":194},"`Only ${",[177,4206,4207],{"class":187},"pool",[177,4209,65],{"class":194},[177,4211,938],{"class":230},[177,4213,4214],{"class":194},"} wallets have enough SOL. Need ${",[177,4216,4217],{"class":187},"numberOfWallets",[177,4219,4220],{"class":194},"}.`",[177,4222,588],{"class":187},[177,4224,4225,4227,4229,4231,4234],{"class":179,"line":350},[177,4226,2084],{"class":183},[177,4228,3164],{"class":237},[177,4230,241],{"class":187},[177,4232,4233],{"class":230},"5000",[177,4235,588],{"class":187},[177,4237,4238,4241],{"class":179,"line":384},[177,4239,4240],{"class":183},"        continue",[177,4242,882],{"class":187},[177,4244,4245],{"class":179,"line":591},[177,4246,967],{"class":187},[177,4248,4249],{"class":179,"line":613},[177,4250,218],{"emptyLinePlaceholder":30},[177,4252,4253],{"class":179,"line":641},[177,4254,4255],{"class":2925},"    // rotation logic\n",[177,4257,4258,4260,4263,4265],{"class":179,"line":647},[177,4259,569],{"class":183},[177,4261,4262],{"class":230}," selectedWallets",[177,4264,234],{"class":183},[177,4266,3638],{"class":187},[177,4268,4269,4272,4274,4276,4279,4281,4283,4286,4288,4291,4294],{"class":179,"line":670},[177,4270,4271],{"class":183},"    for",[177,4273,523],{"class":187},[177,4275,4105],{"class":183},[177,4277,4278],{"class":187}," i ",[177,4280,984],{"class":183},[177,4282,987],{"class":230},[177,4284,4285],{"class":187},"; i ",[177,4287,4143],{"class":183},[177,4289,4290],{"class":187}," numberOfWallets; i",[177,4292,4293],{"class":183},"++",[177,4295,1200],{"class":187},[177,4297,4298,4300,4303,4305,4308,4310,4313,4316,4319,4321],{"class":179,"line":701},[177,4299,1799],{"class":183},[177,4301,4302],{"class":230}," index",[177,4304,234],{"class":183},[177,4306,4307],{"class":187}," (walletOffset ",[177,4309,913],{"class":183},[177,4311,4312],{"class":187}," i) ",[177,4314,4315],{"class":183},"%",[177,4317,4318],{"class":187}," pool.",[177,4320,938],{"class":230},[177,4322,198],{"class":187},[177,4324,4325,4328,4330],{"class":179,"line":720},[177,4326,4327],{"class":187},"        selectedWallets.",[177,4329,3646],{"class":237},[177,4331,4332],{"class":187},"(pool[index]);\n",[177,4334,4335],{"class":179,"line":725},[177,4336,967],{"class":187},[177,4338,4339],{"class":179,"line":738},[177,4340,644],{"class":187},[177,4342,4343],{"class":179,"line":750},[177,4344,4345],{"class":2925},"    // Shift offset\n",[177,4347,4348,4351,4353,4355,4357,4360,4362,4364,4366],{"class":179,"line":787},[177,4349,4350],{"class":187},"    walletOffset ",[177,4352,984],{"class":183},[177,4354,4307],{"class":187},[177,4356,913],{"class":183},[177,4358,4359],{"class":187}," numberOfWallets) ",[177,4361,4315],{"class":183},[177,4363,4318],{"class":187},[177,4365,938],{"class":230},[177,4367,198],{"class":187},[177,4369,4370],{"class":179,"line":793},[177,4371,218],{"emptyLinePlaceholder":30},[177,4373,4374,4376,4379,4381,4384,4386,4388,4391,4393],{"class":179,"line":798},[177,4375,569],{"class":183},[177,4377,4378],{"class":230}," selectedKeypairs",[177,4380,234],{"class":183},[177,4382,4383],{"class":187}," selectedWallets.",[177,4385,2894],{"class":237},[177,4387,241],{"class":187},[177,4389,4390],{"class":526},"entry",[177,4392,555],{"class":183},[177,4394,4395],{"class":187}," entry.wallet);\n",[177,4397,4398],{"class":179,"line":803},[177,4399,218],{"emptyLinePlaceholder":30},[177,4401,4402,4404],{"class":179,"line":842},[177,4403,1792],{"class":183},[177,4405,453],{"class":187},[177,4407,4408],{"class":179,"line":849},[177,4409,4410],{"class":2925},"        // 3. Execute Mass Buy\n",[177,4412,4413,4415,4418,4420,4422,4425,4428,4430],{"class":179,"line":854},[177,4414,1799],{"class":183},[177,4416,4417],{"class":230}," buyResult",[177,4419,234],{"class":183},[177,4421,1600],{"class":183},[177,4423,4424],{"class":237}," massBuy",[177,4426,4427],{"class":187},"(targetMintAddress, maxSolPerWallet, numberOfWallets, password, ",[177,4429,493],{"class":230},[177,4431,4432],{"class":187},", selectedKeypairs);\n",[177,4434,4435],{"class":179,"line":869},[177,4436,218],{"emptyLinePlaceholder":30},[177,4438,4439,4441,4443],{"class":179,"line":885},[177,4440,2156],{"class":187},[177,4442,744],{"class":183},[177,4444,747],{"class":187},[177,4446,4447],{"class":179,"line":900},[177,4448,218],{"emptyLinePlaceholder":30},[177,4450,4451],{"class":179,"line":924},[177,4452,967],{"class":187},[177,4454,4455],{"class":179,"line":929},[177,4456,218],{"emptyLinePlaceholder":30},[177,4458,4459],{"class":179,"line":947},[177,4460,4461],{"class":2925},"    // Mass Sell\n",[177,4463,4464],{"class":179,"line":964},[177,4465,218],{"emptyLinePlaceholder":30},[177,4467,4468,4471,4473,4476,4478,4480],{"class":179,"line":970},[177,4469,4470],{"class":183},"    await",[177,4472,3164],{"class":237},[177,4474,4475],{"class":187},"(interval ",[177,4477,3996],{"class":183},[177,4479,987],{"class":230},[177,4481,588],{"class":187},[177,4483,4484],{"class":179,"line":975},[177,4485,1536],{"class":187},[17,4487,4488,4489,4491],{},"Using the modulo operator, the ",[174,4490,4093],{}," seamlessly wraps back to the beginning of the array once it reaches the end. This ensures every funded sub-wallet gets used evenly, creating much more organic-looking volume on-chain.",[17,4493,4494,4495,4498,4499,4502],{},"Because the worker is detached and has no standard output to the terminal, it continuously writes its current status (buy/sell counts, P&L, current action) to a local ",[174,4496,4497],{},"volumeBot.stats.json"," file. The CLI can then read this file via a ",[174,4500,4501],{},"volume-status"," command to show updates.",[17,4504,4505],{},"Trading with real money means you can't just forcefully kill a process midway through a loop. You might leave wallets holding positions that will crash.",[17,4507,4508,4509,4512,4513,4516],{},"When the user issues the stop command, the CLI sends a ",[174,4510,4511],{},"SIGTERM"," signal to the saved process ID. The worker script listens for this exact signal, flips the stopping boolean to true, which breaks the while loop, and executes a final ",[174,4514,4515],{},"massOutForAllWallets"," to panic-sell and clean up any lingering positions before fully shutting down:",[167,4518,4520],{"className":169,"code":4519,"filename":4098,"language":172,"meta":15,"style":15},"process.on('SIGTERM', () => {\n  stopping = true; \n  await fs.unlink(PID_FILE).catch(() => {});\n});\n\nfinally {\n    if (shouldMassOut) {\n        try {\n            consola.info(\"Closing all positions before shutdown...\");\n            await massOutForAllWallets(password);\n            consola.success(\"Mass out completed.\");\n        } catch (err) {\n            consola.error(\"Mass out failed:\", err);\n        }\n    }\n    await cleanup();\n}\n",[174,4521,4522,4542,4554,4579,4583,4587,4594,4601,4607,4622,4633,4646,4654,4667,4671,4675,4684],{"__ignoreMap":15},[177,4523,4524,4527,4530,4532,4535,4538,4540],{"class":179,"line":180},[177,4525,4526],{"class":187},"process.",[177,4528,4529],{"class":237},"on",[177,4531,241],{"class":187},[177,4533,4534],{"class":194},"'SIGTERM'",[177,4536,4537],{"class":187},", () ",[177,4539,1908],{"class":183},[177,4541,453],{"class":187},[177,4543,4544,4547,4549,4552],{"class":179,"line":25},[177,4545,4546],{"class":187},"  stopping ",[177,4548,984],{"class":183},[177,4550,4551],{"class":230}," true",[177,4553,882],{"class":187},[177,4555,4556,4559,4561,4564,4566,4568,4570,4572,4574,4576],{"class":179,"line":215},[177,4557,4558],{"class":183},"  await",[177,4560,1717],{"class":187},[177,4562,4563],{"class":237},"unlink",[177,4565,241],{"class":187},[177,4567,4042],{"class":230},[177,4569,299],{"class":187},[177,4571,744],{"class":237},[177,4573,3325],{"class":187},[177,4575,1908],{"class":183},[177,4577,4578],{"class":187}," {});\n",[177,4580,4581],{"class":179,"line":221},[177,4582,387],{"class":187},[177,4584,4585],{"class":179,"line":250},[177,4586,218],{"emptyLinePlaceholder":30},[177,4588,4589,4592],{"class":179,"line":285},[177,4590,4591],{"class":183},"finally",[177,4593,453],{"class":187},[177,4595,4596,4598],{"class":179,"line":308},[177,4597,932],{"class":183},[177,4599,4600],{"class":187}," (shouldMassOut) {\n",[177,4602,4603,4605],{"class":179,"line":329},[177,4604,1835],{"class":183},[177,4606,453],{"class":187},[177,4608,4609,4612,4615,4617,4620],{"class":179,"line":350},[177,4610,4611],{"class":187},"            consola.",[177,4613,4614],{"class":237},"info",[177,4616,241],{"class":187},[177,4618,4619],{"class":194},"\"Closing all positions before shutdown...\"",[177,4621,588],{"class":187},[177,4623,4624,4627,4630],{"class":179,"line":384},[177,4625,4626],{"class":183},"            await",[177,4628,4629],{"class":237}," massOutForAllWallets",[177,4631,4632],{"class":187},"(password);\n",[177,4634,4635,4637,4639,4641,4644],{"class":179,"line":591},[177,4636,4611],{"class":187},[177,4638,1517],{"class":237},[177,4640,241],{"class":187},[177,4642,4643],{"class":194},"\"Mass out completed.\"",[177,4645,588],{"class":187},[177,4647,4648,4650,4652],{"class":179,"line":613},[177,4649,2018],{"class":187},[177,4651,744],{"class":183},[177,4653,747],{"class":187},[177,4655,4656,4658,4660,4662,4665],{"class":179,"line":641},[177,4657,4611],{"class":187},[177,4659,1192],{"class":237},[177,4661,241],{"class":187},[177,4663,4664],{"class":194},"\"Mass out failed:\"",[177,4666,3594],{"class":187},[177,4668,4669],{"class":179,"line":647},[177,4670,2041],{"class":187},[177,4672,4673],{"class":179,"line":670},[177,4674,967],{"class":187},[177,4676,4677,4679,4682],{"class":179,"line":701},[177,4678,4470],{"class":183},[177,4680,4681],{"class":237}," cleanup",[177,4683,717],{"class":187},[177,4685,4686],{"class":179,"line":720},[177,4687,1536],{"class":187},[17,4689,4690,4691,4693],{},"With all that was all set, I defined the cli commands with ",[174,4692,164],{}," which is pretty straightforward. Example of the volume bot's start command:",[167,4695,4698],{"className":169,"code":4696,"filename":4697,"language":172,"meta":15,"style":15},"const dataDir =\n  process.platform === \"win32\"\n    ? path.join(process.env[\"APPDATA\"] ?? path.join(os.homedir(), 'AppData', 'Roaming'), \"SolanaBot\")\n    : path.join(os.homedir(), \".solana-bot\");\n\nconst PID_FILE = path.join(dataDir, 'volumeBot.pid');\n\nexport const volumeStart = defineCommand({\n  meta: {\n    name: 'Start Volume Bot',\n    description: 'Start the volume bot in the background',\n  },\n  args: {\n    target: {\n      type: \"string\",\n      required: true,\n      description: \"The SPL token mint address to target (The coin).\",\n      valueHint: \"...\"\n    },\n    wallets: {\n      type: \"string\",\n      required: false,\n      default: \"1\",\n      description: \"Number of sub-wallets to use for volume trading.\",\n      valueHint: \"1\"\n    },\n    amount: {\n      type: \"string\",\n      required: true,\n      description: \"Amount of SOL to use per trade for each wallet.\",\n      default: \"0.01\",\n      valueHint: \"0.01\"\n    },\n    password: {\n      type: \"string\",\n      required: true,\n      description: \"Master password used to decrypt your stored wallets.\",\n      valueHint: \"mySecurePassword\"\n    },\n    interval: {\n      type: \"string\",\n      required: false,\n      description: \"Run a cycle every this much time (ms)\",\n      valueHint: \"1000 for every 1s (default)\"\n    },\n    ttl: {\n      type: \"string\",\n      required: false,\n      default: \"60\",\n      description: \"Optional time-to-live for the bot to run in seconds. if not added the bot will stop after 60s\",\n      valueHint: \"60\"\n    },\n  },\n\n  async run({ args }) {\n    try {\n          const existingPid = Number(await fs.readFile(PID_FILE, 'utf8'));\n          process.kill(existingPid, 0);\n          consola.warn(`Volume bot already running: ${existingPid}`);\n          return;\n    } catch { }\n    \n        await fs.mkdir(dataDir, { recursive: true });\n        const child = spawn(process.execPath, process.argv.slice(1), {\n            detached: true,\n            stdio: \"ignore\",\n            env: {\n                ...process.env,\n                SOLANA_BOTS_INTERNAL: \"volume-worker\",\n                TARGET_MINT: args.target,\n                WALLETS: args.wallets,\n                AMOUNT: args.amount,\n                PASSWORD: args.password,\n                INTERVAL: args.interval,\n                TTL: args.ttl ?? \"\",\n            },\n        });\n\n        child.unref();\n\n        await fs.writeFile(PID_FILE, String(child.pid));\n        consola.success(`Volume bot started in background. PID: ${child.pid}`);\n  },\n  \n});\n","src/lib/transactions/volumeBot/volumeStart.ts",[174,4699,4700,4710,4720,4770,4790,4794,4815,4819,4835,4840,4850,4860,4865,4870,4875,4885,4894,4904,4912,4916,4921,4929,4937,4947,4956,4963,4967,4972,4980,4988,4997,5006,5013,5017,5022,5030,5038,5047,5054,5058,5063,5071,5079,5088,5095,5099,5104,5112,5120,5129,5138,5145,5149,5153,5157,5174,5180,5213,5229,5248,5256,5266,5271,5289,5310,5320,5330,5336,5344,5354,5360,5366,5372,5378,5384,5396,5402,5407,5412,5422,5427,5446,5467,5472,5478],{"__ignoreMap":15},[177,4701,4702,4704,4707],{"class":179,"line":180},[177,4703,445],{"class":183},[177,4705,4706],{"class":230}," dataDir",[177,4708,4709],{"class":183}," =\n",[177,4711,4712,4715,4717],{"class":179,"line":25},[177,4713,4714],{"class":187},"  process.platform ",[177,4716,1268],{"class":183},[177,4718,4719],{"class":194}," \"win32\"\n",[177,4721,4722,4725,4727,4730,4733,4736,4738,4740,4742,4744,4747,4750,4753,4756,4758,4761,4764,4767],{"class":179,"line":215},[177,4723,4724],{"class":183},"    ?",[177,4726,1693],{"class":187},[177,4728,4729],{"class":237},"join",[177,4731,4732],{"class":187},"(process.env[",[177,4734,4735],{"class":194},"\"APPDATA\"",[177,4737,1595],{"class":187},[177,4739,3996],{"class":183},[177,4741,1693],{"class":187},[177,4743,4729],{"class":237},[177,4745,4746],{"class":187},"(os.",[177,4748,4749],{"class":237},"homedir",[177,4751,4752],{"class":187},"(), ",[177,4754,4755],{"class":194},"'AppData'",[177,4757,536],{"class":187},[177,4759,4760],{"class":194},"'Roaming'",[177,4762,4763],{"class":187},"), ",[177,4765,4766],{"class":194},"\"SolanaBot\"",[177,4768,4769],{"class":187},")\n",[177,4771,4772,4775,4777,4779,4781,4783,4785,4788],{"class":179,"line":221},[177,4773,4774],{"class":183},"    :",[177,4776,1693],{"class":187},[177,4778,4729],{"class":237},[177,4780,4746],{"class":187},[177,4782,4749],{"class":237},[177,4784,4752],{"class":187},[177,4786,4787],{"class":194},"\".solana-bot\"",[177,4789,588],{"class":187},[177,4791,4792],{"class":179,"line":250},[177,4793,218],{"emptyLinePlaceholder":30},[177,4795,4796,4798,4801,4803,4805,4807,4810,4813],{"class":179,"line":285},[177,4797,445],{"class":183},[177,4799,4800],{"class":230}," PID_FILE",[177,4802,234],{"class":183},[177,4804,1693],{"class":187},[177,4806,4729],{"class":237},[177,4808,4809],{"class":187},"(dataDir, ",[177,4811,4812],{"class":194},"'volumeBot.pid'",[177,4814,588],{"class":187},[177,4816,4817],{"class":179,"line":308},[177,4818,218],{"emptyLinePlaceholder":30},[177,4820,4821,4823,4825,4828,4830,4833],{"class":179,"line":329},[177,4822,224],{"class":183},[177,4824,227],{"class":183},[177,4826,4827],{"class":230}," volumeStart",[177,4829,234],{"class":183},[177,4831,4832],{"class":237}," defineCommand",[177,4834,1478],{"class":187},[177,4836,4837],{"class":179,"line":350},[177,4838,4839],{"class":187},"  meta: {\n",[177,4841,4842,4845,4848],{"class":179,"line":384},[177,4843,4844],{"class":187},"    name: ",[177,4846,4847],{"class":194},"'Start Volume Bot'",[177,4849,464],{"class":187},[177,4851,4852,4855,4858],{"class":179,"line":591},[177,4853,4854],{"class":187},"    description: ",[177,4856,4857],{"class":194},"'Start the volume bot in the background'",[177,4859,464],{"class":187},[177,4861,4862],{"class":179,"line":613},[177,4863,4864],{"class":187},"  },\n",[177,4866,4867],{"class":179,"line":641},[177,4868,4869],{"class":187},"  args: {\n",[177,4871,4872],{"class":179,"line":647},[177,4873,4874],{"class":187},"    target: {\n",[177,4876,4877,4880,4883],{"class":179,"line":670},[177,4878,4879],{"class":187},"      type: ",[177,4881,4882],{"class":194},"\"string\"",[177,4884,464],{"class":187},[177,4886,4887,4890,4892],{"class":179,"line":701},[177,4888,4889],{"class":187},"      required: ",[177,4891,279],{"class":230},[177,4893,464],{"class":187},[177,4895,4896,4899,4902],{"class":179,"line":720},[177,4897,4898],{"class":187},"      description: ",[177,4900,4901],{"class":194},"\"The SPL token mint address to target (The coin).\"",[177,4903,464],{"class":187},[177,4905,4906,4909],{"class":179,"line":725},[177,4907,4908],{"class":187},"      valueHint: ",[177,4910,4911],{"class":194},"\"...\"\n",[177,4913,4914],{"class":179,"line":738},[177,4915,4006],{"class":187},[177,4917,4918],{"class":179,"line":750},[177,4919,4920],{"class":187},"    wallets: {\n",[177,4922,4923,4925,4927],{"class":179,"line":787},[177,4924,4879],{"class":187},[177,4926,4882],{"class":194},[177,4928,464],{"class":187},[177,4930,4931,4933,4935],{"class":179,"line":793},[177,4932,4889],{"class":187},[177,4934,3802],{"class":230},[177,4936,464],{"class":187},[177,4938,4939,4942,4945],{"class":179,"line":798},[177,4940,4941],{"class":187},"      default: ",[177,4943,4944],{"class":194},"\"1\"",[177,4946,464],{"class":187},[177,4948,4949,4951,4954],{"class":179,"line":803},[177,4950,4898],{"class":187},[177,4952,4953],{"class":194},"\"Number of sub-wallets to use for volume trading.\"",[177,4955,464],{"class":187},[177,4957,4958,4960],{"class":179,"line":842},[177,4959,4908],{"class":187},[177,4961,4962],{"class":194},"\"1\"\n",[177,4964,4965],{"class":179,"line":849},[177,4966,4006],{"class":187},[177,4968,4969],{"class":179,"line":854},[177,4970,4971],{"class":187},"    amount: {\n",[177,4973,4974,4976,4978],{"class":179,"line":869},[177,4975,4879],{"class":187},[177,4977,4882],{"class":194},[177,4979,464],{"class":187},[177,4981,4982,4984,4986],{"class":179,"line":885},[177,4983,4889],{"class":187},[177,4985,279],{"class":230},[177,4987,464],{"class":187},[177,4989,4990,4992,4995],{"class":179,"line":900},[177,4991,4898],{"class":187},[177,4993,4994],{"class":194},"\"Amount of SOL to use per trade for each wallet.\"",[177,4996,464],{"class":187},[177,4998,4999,5001,5004],{"class":179,"line":924},[177,5000,4941],{"class":187},[177,5002,5003],{"class":194},"\"0.01\"",[177,5005,464],{"class":187},[177,5007,5008,5010],{"class":179,"line":929},[177,5009,4908],{"class":187},[177,5011,5012],{"class":194},"\"0.01\"\n",[177,5014,5015],{"class":179,"line":947},[177,5016,4006],{"class":187},[177,5018,5019],{"class":179,"line":964},[177,5020,5021],{"class":187},"    password: {\n",[177,5023,5024,5026,5028],{"class":179,"line":970},[177,5025,4879],{"class":187},[177,5027,4882],{"class":194},[177,5029,464],{"class":187},[177,5031,5032,5034,5036],{"class":179,"line":975},[177,5033,4889],{"class":187},[177,5035,279],{"class":230},[177,5037,464],{"class":187},[177,5039,5040,5042,5045],{"class":179,"line":992},[177,5041,4898],{"class":187},[177,5043,5044],{"class":194},"\"Master password used to decrypt your stored wallets.\"",[177,5046,464],{"class":187},[177,5048,5049,5051],{"class":179,"line":1016},[177,5050,4908],{"class":187},[177,5052,5053],{"class":194},"\"mySecurePassword\"\n",[177,5055,5056],{"class":179,"line":1036},[177,5057,4006],{"class":187},[177,5059,5060],{"class":179,"line":1056},[177,5061,5062],{"class":187},"    interval: {\n",[177,5064,5065,5067,5069],{"class":179,"line":1073},[177,5066,4879],{"class":187},[177,5068,4882],{"class":194},[177,5070,464],{"class":187},[177,5072,5073,5075,5077],{"class":179,"line":1078},[177,5074,4889],{"class":187},[177,5076,3802],{"class":230},[177,5078,464],{"class":187},[177,5080,5081,5083,5086],{"class":179,"line":1101},[177,5082,4898],{"class":187},[177,5084,5085],{"class":194},"\"Run a cycle every this much time (ms)\"",[177,5087,464],{"class":187},[177,5089,5090,5092],{"class":179,"line":1122},[177,5091,4908],{"class":187},[177,5093,5094],{"class":194},"\"1000 for every 1s (default)\"\n",[177,5096,5097],{"class":179,"line":1134},[177,5098,4006],{"class":187},[177,5100,5101],{"class":179,"line":1139},[177,5102,5103],{"class":187},"    ttl: {\n",[177,5105,5106,5108,5110],{"class":179,"line":1156},[177,5107,4879],{"class":187},[177,5109,4882],{"class":194},[177,5111,464],{"class":187},[177,5113,5114,5116,5118],{"class":179,"line":1167},[177,5115,4889],{"class":187},[177,5117,3802],{"class":230},[177,5119,464],{"class":187},[177,5121,5122,5124,5127],{"class":179,"line":1177},[177,5123,4941],{"class":187},[177,5125,5126],{"class":194},"\"60\"",[177,5128,464],{"class":187},[177,5130,5131,5133,5136],{"class":179,"line":1183},[177,5132,4898],{"class":187},[177,5134,5135],{"class":194},"\"Optional time-to-live for the bot to run in seconds. if not added the bot will stop after 60s\"",[177,5137,464],{"class":187},[177,5139,5140,5142],{"class":179,"line":1203},[177,5141,4908],{"class":187},[177,5143,5144],{"class":194},"\"60\"\n",[177,5146,5147],{"class":179,"line":1233},[177,5148,4006],{"class":187},[177,5150,5151],{"class":179,"line":1260},[177,5152,4864],{"class":187},[177,5154,5155],{"class":179,"line":1296},[177,5156,218],{"emptyLinePlaceholder":30},[177,5158,5159,5162,5165,5168,5171],{"class":179,"line":1312},[177,5160,5161],{"class":183},"  async",[177,5163,5164],{"class":237}," run",[177,5166,5167],{"class":187},"({ ",[177,5169,5170],{"class":526},"args",[177,5172,5173],{"class":187}," }) {\n",[177,5175,5176,5178],{"class":179,"line":1317},[177,5177,1792],{"class":183},[177,5179,453],{"class":187},[177,5181,5182,5185,5188,5190,5193,5195,5197,5199,5202,5204,5206,5208,5211],{"class":179,"line":1333},[177,5183,5184],{"class":183},"          const",[177,5186,5187],{"class":230}," existingPid",[177,5189,234],{"class":183},[177,5191,5192],{"class":237}," Number",[177,5194,241],{"class":187},[177,5196,2852],{"class":183},[177,5198,1717],{"class":187},[177,5200,5201],{"class":237},"readFile",[177,5203,241],{"class":187},[177,5205,4042],{"class":230},[177,5207,536],{"class":187},[177,5209,5210],{"class":194},"'utf8'",[177,5212,1627],{"class":187},[177,5214,5215,5218,5221,5224,5227],{"class":179,"line":1341},[177,5216,5217],{"class":187},"          process.",[177,5219,5220],{"class":237},"kill",[177,5222,5223],{"class":187},"(existingPid, ",[177,5225,5226],{"class":230},"0",[177,5228,588],{"class":187},[177,5230,5231,5234,5236,5238,5241,5244,5246],{"class":179,"line":1346},[177,5232,5233],{"class":187},"          consola.",[177,5235,2647],{"class":237},[177,5237,241],{"class":187},[177,5239,5240],{"class":194},"`Volume bot already running: ${",[177,5242,5243],{"class":187},"existingPid",[177,5245,782],{"class":194},[177,5247,588],{"class":187},[177,5249,5251,5254],{"class":179,"line":5250},60,[177,5252,5253],{"class":183},"          return",[177,5255,198],{"class":187},[177,5257,5259,5261,5263],{"class":179,"line":5258},61,[177,5260,2156],{"class":187},[177,5262,744],{"class":183},[177,5264,5265],{"class":187}," { }\n",[177,5267,5269],{"class":179,"line":5268},62,[177,5270,644],{"class":187},[177,5272,5274,5276,5278,5281,5284,5286],{"class":179,"line":5273},63,[177,5275,2084],{"class":183},[177,5277,1717],{"class":187},[177,5279,5280],{"class":237},"mkdir",[177,5282,5283],{"class":187},"(dataDir, { recursive: ",[177,5285,279],{"class":230},[177,5287,5288],{"class":187}," });\n",[177,5290,5292,5294,5296,5298,5300,5302,5304,5306,5308],{"class":179,"line":5291},64,[177,5293,1799],{"class":183},[177,5295,3903],{"class":230},[177,5297,234],{"class":183},[177,5299,3908],{"class":237},[177,5301,3911],{"class":187},[177,5303,3914],{"class":237},[177,5305,241],{"class":187},[177,5307,493],{"class":230},[177,5309,3921],{"class":187},[177,5311,5313,5316,5318],{"class":179,"line":5312},65,[177,5314,5315],{"class":187},"            detached: ",[177,5317,279],{"class":230},[177,5319,464],{"class":187},[177,5321,5323,5326,5328],{"class":179,"line":5322},66,[177,5324,5325],{"class":187},"            stdio: ",[177,5327,3938],{"class":194},[177,5329,464],{"class":187},[177,5331,5333],{"class":179,"line":5332},67,[177,5334,5335],{"class":187},"            env: {\n",[177,5337,5339,5342],{"class":179,"line":5338},68,[177,5340,5341],{"class":183},"                ...",[177,5343,3953],{"class":187},[177,5345,5347,5350,5352],{"class":179,"line":5346},69,[177,5348,5349],{"class":187},"                SOLANA_BOTS_INTERNAL: ",[177,5351,3961],{"class":194},[177,5353,464],{"class":187},[177,5355,5357],{"class":179,"line":5356},70,[177,5358,5359],{"class":187},"                TARGET_MINT: args.target,\n",[177,5361,5363],{"class":179,"line":5362},71,[177,5364,5365],{"class":187},"                WALLETS: args.wallets,\n",[177,5367,5369],{"class":179,"line":5368},72,[177,5370,5371],{"class":187},"                AMOUNT: args.amount,\n",[177,5373,5375],{"class":179,"line":5374},73,[177,5376,5377],{"class":187},"                PASSWORD: args.password,\n",[177,5379,5381],{"class":179,"line":5380},74,[177,5382,5383],{"class":187},"                INTERVAL: args.interval,\n",[177,5385,5387,5390,5392,5394],{"class":179,"line":5386},75,[177,5388,5389],{"class":187},"                TTL: args.ttl ",[177,5391,3996],{"class":183},[177,5393,3999],{"class":194},[177,5395,464],{"class":187},[177,5397,5399],{"class":179,"line":5398},76,[177,5400,5401],{"class":187},"            },\n",[177,5403,5405],{"class":179,"line":5404},77,[177,5406,2121],{"class":187},[177,5408,5410],{"class":179,"line":5409},78,[177,5411,218],{"emptyLinePlaceholder":30},[177,5413,5415,5418,5420],{"class":179,"line":5414},79,[177,5416,5417],{"class":187},"        child.",[177,5419,4022],{"class":237},[177,5421,717],{"class":187},[177,5423,5425],{"class":179,"line":5424},80,[177,5426,218],{"emptyLinePlaceholder":30},[177,5428,5430,5432,5434,5436,5438,5440,5442,5444],{"class":179,"line":5429},81,[177,5431,2084],{"class":183},[177,5433,1717],{"class":187},[177,5435,4037],{"class":237},[177,5437,241],{"class":187},[177,5439,4042],{"class":230},[177,5441,536],{"class":187},[177,5443,2949],{"class":237},[177,5445,4049],{"class":187},[177,5447,5449,5451,5453,5455,5457,5459,5461,5463,5465],{"class":179,"line":5448},82,[177,5450,1649],{"class":187},[177,5452,1517],{"class":237},[177,5454,241],{"class":187},[177,5456,4061],{"class":194},[177,5458,4064],{"class":187},[177,5460,65],{"class":194},[177,5462,4069],{"class":187},[177,5464,782],{"class":194},[177,5466,588],{"class":187},[177,5468,5470],{"class":179,"line":5469},83,[177,5471,4864],{"class":187},[177,5473,5475],{"class":179,"line":5474},84,[177,5476,5477],{"class":187},"  \n",[177,5479,5481],{"class":179,"line":5480},85,[177,5482,387],{"class":187},[17,5484,5485,5486,5488,5489,530],{},"You then simply import all your commands into a single ",[174,5487,3862],{}," file, and call ",[174,5490,5491],{},"runMain(main)",[167,5493,5495],{"className":169,"code":5494,"filename":3862,"language":172,"meta":15,"style":15},"if (process.env['SOLANA_BOTS_INTERNAL'] === 'volume-worker') {\n  try {\n    await volumeBot(\n      process.env['TARGET_MINT']!,\n      Number(process.env['AMOUNT']),\n      Number(process.env['WALLETS']),\n      process.env['PASSWORD']!,\n      Number(process.env['INTERVAL']),\n      (process.env['MASS_OUT'] === 'true' ? true : false),\n      process.env['TTL'] ? Number(process.env['TTL']) : undefined,\n    );\n  } catch (err) {\n    consola.error(err);\n    process.exitCode = 1;\n  }\n  process.exit(0);\n}\n\nconst start = defineCommand({\n  meta: {\n    name: 'Solana Bots',\n    description: 'Start the wizard',\n  },\n  subCommands: {\n    main: storeMainWallet,\n    create: createWallets,\n    'api-rpc': apiAndRpc,\n    drain: drainWallets,\n    split: splitFounds,\n    'read-all': readAllWallets,\n    read: readWalletView,\n    'volume-start': volumeStart,\n    'volume-stop': volumeStop,\n    'volume-status': volumeStatus,\n    airdrop: getAirDropC,\n    buy,\n    sell,\n    out: massOut\n  },\n\n  async run({args}) {\n   if (args._ && args._.length > 0) {\n        return; \n    }\n\n     consola.box('Solana Bots - Starter');\n     // you got the idea\n  }\n})\nawait runMain(start);\n",[174,5496,5497,5516,5522,5531,5546,5559,5570,5583,5594,5620,5647,5652,5660,5668,5679,5683,5697,5701,5705,5718,5722,5731,5740,5744,5749,5754,5759,5767,5772,5777,5785,5790,5798,5806,5814,5819,5824,5829,5834,5838,5842,5856,5877,5883,5887,5891,5906,5911,5915,5920],{"__ignoreMap":15},[177,5498,5499,5501,5504,5507,5509,5511,5514],{"class":179,"line":180},[177,5500,3468],{"class":183},[177,5502,5503],{"class":187}," (process.env[",[177,5505,5506],{"class":194},"'SOLANA_BOTS_INTERNAL'",[177,5508,1595],{"class":187},[177,5510,1268],{"class":183},[177,5512,5513],{"class":194}," 'volume-worker'",[177,5515,1200],{"class":187},[177,5517,5518,5520],{"class":179,"line":25},[177,5519,562],{"class":183},[177,5521,453],{"class":187},[177,5523,5524,5526,5529],{"class":179,"line":215},[177,5525,4470],{"class":183},[177,5527,5528],{"class":237}," volumeBot",[177,5530,2886],{"class":187},[177,5532,5533,5536,5539,5542,5544],{"class":179,"line":221},[177,5534,5535],{"class":187},"      process.env[",[177,5537,5538],{"class":194},"'TARGET_MINT'",[177,5540,5541],{"class":187},"]",[177,5543,1741],{"class":183},[177,5545,464],{"class":187},[177,5547,5548,5551,5553,5556],{"class":179,"line":250},[177,5549,5550],{"class":237},"      Number",[177,5552,4732],{"class":187},[177,5554,5555],{"class":194},"'AMOUNT'",[177,5557,5558],{"class":187},"]),\n",[177,5560,5561,5563,5565,5568],{"class":179,"line":285},[177,5562,5550],{"class":237},[177,5564,4732],{"class":187},[177,5566,5567],{"class":194},"'WALLETS'",[177,5569,5558],{"class":187},[177,5571,5572,5574,5577,5579,5581],{"class":179,"line":308},[177,5573,5535],{"class":187},[177,5575,5576],{"class":194},"'PASSWORD'",[177,5578,5541],{"class":187},[177,5580,1741],{"class":183},[177,5582,464],{"class":187},[177,5584,5585,5587,5589,5592],{"class":179,"line":329},[177,5586,5550],{"class":237},[177,5588,4732],{"class":187},[177,5590,5591],{"class":194},"'INTERVAL'",[177,5593,5558],{"class":187},[177,5595,5596,5599,5602,5604,5606,5609,5611,5613,5615,5618],{"class":179,"line":350},[177,5597,5598],{"class":187},"      (process.env[",[177,5600,5601],{"class":194},"'MASS_OUT'",[177,5603,1595],{"class":187},[177,5605,1268],{"class":183},[177,5607,5608],{"class":194}," 'true'",[177,5610,1221],{"class":183},[177,5612,4551],{"class":230},[177,5614,3703],{"class":183},[177,5616,5617],{"class":230}," false",[177,5619,381],{"class":187},[177,5621,5622,5624,5627,5629,5631,5633,5635,5637,5640,5642,5645],{"class":179,"line":384},[177,5623,5535],{"class":187},[177,5625,5626],{"class":194},"'TTL'",[177,5628,1595],{"class":187},[177,5630,3696],{"class":183},[177,5632,5192],{"class":237},[177,5634,4732],{"class":187},[177,5636,5626],{"class":194},[177,5638,5639],{"class":187},"]) ",[177,5641,530],{"class":183},[177,5643,5644],{"class":230}," undefined",[177,5646,464],{"class":187},[177,5648,5649],{"class":179,"line":591},[177,5650,5651],{"class":187},"    );\n",[177,5653,5654,5656,5658],{"class":179,"line":613},[177,5655,741],{"class":187},[177,5657,744],{"class":183},[177,5659,747],{"class":187},[177,5661,5662,5664,5666],{"class":179,"line":641},[177,5663,1320],{"class":187},[177,5665,1192],{"class":237},[177,5667,2182],{"class":187},[177,5669,5670,5673,5675,5677],{"class":179,"line":647},[177,5671,5672],{"class":187},"    process.exitCode ",[177,5674,984],{"class":183},[177,5676,2725],{"class":230},[177,5678,198],{"class":187},[177,5680,5681],{"class":179,"line":670},[177,5682,790],{"class":187},[177,5684,5685,5688,5691,5693,5695],{"class":179,"line":701},[177,5686,5687],{"class":187},"  process.",[177,5689,5690],{"class":237},"exit",[177,5692,241],{"class":187},[177,5694,5226],{"class":230},[177,5696,588],{"class":187},[177,5698,5699],{"class":179,"line":720},[177,5700,1536],{"class":187},[177,5702,5703],{"class":179,"line":725},[177,5704,218],{"emptyLinePlaceholder":30},[177,5706,5707,5709,5712,5714,5716],{"class":179,"line":738},[177,5708,445],{"class":183},[177,5710,5711],{"class":230}," start",[177,5713,234],{"class":183},[177,5715,4832],{"class":237},[177,5717,1478],{"class":187},[177,5719,5720],{"class":179,"line":750},[177,5721,4839],{"class":187},[177,5723,5724,5726,5729],{"class":179,"line":787},[177,5725,4844],{"class":187},[177,5727,5728],{"class":194},"'Solana Bots'",[177,5730,464],{"class":187},[177,5732,5733,5735,5738],{"class":179,"line":793},[177,5734,4854],{"class":187},[177,5736,5737],{"class":194},"'Start the wizard'",[177,5739,464],{"class":187},[177,5741,5742],{"class":179,"line":798},[177,5743,4864],{"class":187},[177,5745,5746],{"class":179,"line":803},[177,5747,5748],{"class":187},"  subCommands: {\n",[177,5750,5751],{"class":179,"line":842},[177,5752,5753],{"class":187},"    main: storeMainWallet,\n",[177,5755,5756],{"class":179,"line":849},[177,5757,5758],{"class":187},"    create: createWallets,\n",[177,5760,5761,5764],{"class":179,"line":854},[177,5762,5763],{"class":194},"    'api-rpc'",[177,5765,5766],{"class":187},": apiAndRpc,\n",[177,5768,5769],{"class":179,"line":869},[177,5770,5771],{"class":187},"    drain: drainWallets,\n",[177,5773,5774],{"class":179,"line":885},[177,5775,5776],{"class":187},"    split: splitFounds,\n",[177,5778,5779,5782],{"class":179,"line":900},[177,5780,5781],{"class":194},"    'read-all'",[177,5783,5784],{"class":187},": readAllWallets,\n",[177,5786,5787],{"class":179,"line":924},[177,5788,5789],{"class":187},"    read: readWalletView,\n",[177,5791,5792,5795],{"class":179,"line":929},[177,5793,5794],{"class":194},"    'volume-start'",[177,5796,5797],{"class":187},": volumeStart,\n",[177,5799,5800,5803],{"class":179,"line":947},[177,5801,5802],{"class":194},"    'volume-stop'",[177,5804,5805],{"class":187},": volumeStop,\n",[177,5807,5808,5811],{"class":179,"line":964},[177,5809,5810],{"class":194},"    'volume-status'",[177,5812,5813],{"class":187},": volumeStatus,\n",[177,5815,5816],{"class":179,"line":970},[177,5817,5818],{"class":187},"    airdrop: getAirDropC,\n",[177,5820,5821],{"class":179,"line":975},[177,5822,5823],{"class":187},"    buy,\n",[177,5825,5826],{"class":179,"line":992},[177,5827,5828],{"class":187},"    sell,\n",[177,5830,5831],{"class":179,"line":1016},[177,5832,5833],{"class":187},"    out: massOut\n",[177,5835,5836],{"class":179,"line":1036},[177,5837,4864],{"class":187},[177,5839,5840],{"class":179,"line":1056},[177,5841,218],{"emptyLinePlaceholder":30},[177,5843,5844,5846,5848,5851,5853],{"class":179,"line":1073},[177,5845,5161],{"class":183},[177,5847,5164],{"class":237},[177,5849,5850],{"class":187},"({",[177,5852,5170],{"class":526},[177,5854,5855],{"class":187},"}) {\n",[177,5857,5858,5861,5864,5866,5869,5871,5873,5875],{"class":179,"line":1078},[177,5859,5860],{"class":183},"   if",[177,5862,5863],{"class":187}," (args._ ",[177,5865,1890],{"class":183},[177,5867,5868],{"class":187}," args._.",[177,5870,938],{"class":230},[177,5872,3689],{"class":183},[177,5874,987],{"class":230},[177,5876,1200],{"class":187},[177,5878,5879,5881],{"class":179,"line":1101},[177,5880,2149],{"class":183},[177,5882,882],{"class":187},[177,5884,5885],{"class":179,"line":1122},[177,5886,967],{"class":187},[177,5888,5889],{"class":179,"line":1134},[177,5890,218],{"emptyLinePlaceholder":30},[177,5892,5893,5896,5899,5901,5904],{"class":179,"line":1139},[177,5894,5895],{"class":187},"     consola.",[177,5897,5898],{"class":237},"box",[177,5900,241],{"class":187},[177,5902,5903],{"class":194},"'Solana Bots - Starter'",[177,5905,588],{"class":187},[177,5907,5908],{"class":179,"line":1156},[177,5909,5910],{"class":2925},"     // you got the idea\n",[177,5912,5913],{"class":179,"line":1167},[177,5914,790],{"class":187},[177,5916,5917],{"class":179,"line":1177},[177,5918,5919],{"class":187},"})\n",[177,5921,5922,5924,5927],{"class":179,"line":1183},[177,5923,2852],{"class":183},[177,5925,5926],{"class":237}," runMain",[177,5928,5929],{"class":187},"(start);\n",[17,5931,5932,5935],{},[53,5933,164],{"href":162,"rel":5934},[57]," automatically generates a help menu for you, for every single command.",[17,5937,5938,5939,5944,5945,5948],{},"I was then configuring ",[53,5940,5943],{"href":5941,"rel":5942},"https://github.com/vercel/pkg",[57],"pkg"," which was a pain in the ass (I didn't notice it was archived), it doesn't support esm, newer versions of node, and the list goes on, so I ditched it, and used ",[174,5946,5947],{},"bun"," instead, that resulted in a faster build, and cli overall. That also means I needed to switch to bun's built in sqlite instead of better-sqlite3.",[17,5950,5951],{},"My friend tried it. As expected, managing multiple configuration flags, reading JSON logs, and interacting via terminal commands was a bit too much for a non-technical user. It was time to wrap all of this into a nice, friendly desktop app using Electron.",[67,5953,5955],{"id":5954},"the-electron-experience","The Electron Experience",[17,5957,5958],{},"While Electron is extremely powerful, and really an awesome framework (I was really impressed by it), its complexity comes from the boilerplate it requires you to wire, and its build tools.",[17,5960,5961],{},"Setting up the dev environment is pretty easy, you run:",[167,5963,5967],{"className":5964,"code":5965,"language":5966,"meta":15,"style":15},"language-bash shiki shiki-themes github-light github-dark github-dark","npx create-electron-app@latest my-new-app --template=vite-typescript\n","bash",[174,5968,5969],{"__ignoreMap":15},[177,5970,5971,5974,5977,5980],{"class":179,"line":180},[177,5972,5973],{"class":237},"npx",[177,5975,5976],{"class":194}," create-electron-app@latest",[177,5978,5979],{"class":194}," my-new-app",[177,5981,5982],{"class":230}," --template=vite-typescript\n",[17,5984,5985,5986,5990,5991,65],{},"That creates the Vite setup using electron forge ",[53,5987,13],{"href":5988,"rel":5989},"https://www.electronforge.io/config/plugins/vite#native-node-modules",[57],", and since I used Vue, for this app I followed the ",[53,5992,5995],{"href":5993,"rel":5994},"https://www.electronforge.io/guides/framework-integration/vue-3",[57],"Vue integration guide",[17,5997,5998,5999,6004,6005,1543,6010,6015],{},"After exploring and reading the forge docs a bit, I realized that the build process should end up pretty simple, so I continued with the frontend development without thinking much about it. I used ",[53,6000,6003],{"href":6001,"rel":6002},"https://ui.nuxt.com/",[57],"Nuxt UI"," ",[53,6006,6009],{"href":6007,"rel":6008},"https://tailwindcss.com/",[57],"Tailwind",[53,6011,6014],{"href":6012,"rel":6013},"https://router.vuejs.org/",[57],"Vue Router",", so I was able to move fast and complete the frontend pretty quickly.",[17,6017,6018,6019,6024,6025,6028,6029,1543,6032,6037,6038,65],{},"I then started to test it to see how the backend of the system interacts with the frontend. Discovered ",[53,6020,6023],{"href":6021,"rel":6022},"https://www.electronjs.org/docs/latest/tutorial/ipc",[57],"Electron's IPC"," model that you should provide your ",[174,6026,6027],{},"renderer.ts"," explicitly the methods you want it to have from the ",[174,6030,6031],{},"preload.ts",[53,6033,6036],{"href":6034,"rel":6035},"https://www.electronjs.org/docs/latest/tutorial/context-isolation",[57],"context isolation",". And communication between the frontend and the main process or vice versa, only via ",[174,6039,6031],{},[17,6041,6042,6043,6045,6046,65],{},"So that is what I did. I wired everything up so that the components and forms from the renderer call the methods defined in ",[174,6044,6031],{},", which in turn trigger the main process via ",[174,6047,6048],{},"ipcRenderer.invoke()",[17,6050,6051],{},"However, for the Volume Bot, the approach had to be a bit different.",[17,6053,6054,6055,6057,6058,65],{},"Because the volume bot runs a continuous, heavy background loop dealing with cryptography, network requests, and database writes, running it directly inside Electron's main process would completely choke up the application. If the main process blocks, the entire UI freezes. In the CLI version, I used Node's ",[174,6056,3857],{},", but Electron provides a dedicated API specifically designed for running heavy, Node.js-dependent background tasks: the ",[53,6059,6062],{"href":6060,"rel":6061},"https://www.electronjs.org/docs/latest/api/utility-process",[57],[174,6063,6064],{},"utilityProcess",[17,6066,6067],{},"To bridge the gap between this detached utility process and the Vue frontend, I needed a way to stream real-time stats and logs so the user could see what the bot was doing.",[17,6069,6070,6071,299],{},"Instead of setting up complex message ports, I used a simpler approach: standard output stream parsing. Inside the worker script, whenever the bot updates its stats, it simply logs a stringified JSON object prefixed with a special token (",[174,6072,6073],{},"__BOT_METRIC__:",[17,6075,6076],{},"Here is how I implemented the bridge in the main Electron process:",[167,6078,6081],{"className":169,"code":6079,"filename":6080,"language":172,"meta":15,"style":15},"\nimport { ipcMain, safeStorage, utilityProcess } from 'electron';\nimport readline from 'node:readline';\n// ...\n\nexport function setupVolumeBotBridge(mainWindow: BrowserWindow, getVault: () => string | null) {\n  \n ipcMain.handle('trade:volume-bot', async (_event, targetMintAddress, maxSolPerWallet, numberOfWallets, interval, shouldMassOut, ttl) => {\n    const vault = getVault();\n    if (!vault) return { ok: false, reason: 'UNAUTHORIZED'};\n    const decryptedPassword = safeStorage.decryptString(Buffer.from(vault, 'base64'));  \n\n    //fork heavy worker\n    workerThread = utilityProcess.fork(cliScriptPath, [], {\n        env: {\n          ...process.env,\n          SOLANA_BOTS_INTERNAL: 'volume-worker',\n          TARGET_MINT: targetMintAddress,\n          AMOUNT: String(maxSolPerWallet),\n          WALLETS: String(numberOfWallets),\n          PASSWORD: decryptedPassword,\n          INTERVAL: String(interval ?? 0),\n          MASS_OUT: String(shouldMassOut),\n          TTL: ttl ? String(ttl) : \"\"\n        },\n        stdio: 'pipe'\n    });\n      \n    if (workerThread.stdout) {\n        // read the output\n        const stdoutReader = readline.createInterface({ input: workerThread.stdout });\n        \n        stdoutReader.on('line', (line) => {\n          const cleanLine = line.trim();\n          if (!cleanLine) return;\n\n          // metric token\n          if (cleanLine.startsWith('__BOT_METRIC__:')) {\n            try {\n              const event = JSON.parse(cleanLine.replace('__BOT_METRIC__:', ''));\n              \n              // Route the data to the frontend\n              switch (event.type) {\n                case 'stats':\n                  mainWindow.webContents.send('volume-bot:stats', event.payload);\n                  break;\n                case 'log':\n                  mainWindow.webContents.send('volume-bot:log', event.payload);\n                  break;\n                case 'activity':\n                  mainWindow.webContents.send('volume-bot:activity', event.payload);\n                  break;\n                case 'status':\n                  mainWindow.webContents.send('volume-bot:status', event.payload);\n                  break;\n              }\n            } catch (err) {\n              console.error('Failed to parse bot metric line:', err);\n            }\n          } else {\n             // pass standard console logs directly to the UI log viewer\n            mainWindow.webContents.send('volume-bot:raw-console', cleanLine);\n          }\n        });\n      }\n\n      workerThread.on('exit', () => {\n        mainWindow.webContents.send('volume-bot:status', { status: 'stopped', stoppedAt: Date.now() });\n        workerThread = null;\n      });\n\n      return { ok: true, data: 'SUCCESS'};\n  });\n  \n  // stop logic\n}\n","volumeBotIPC.ts",[174,6082,6083,6087,6101,6115,6120,6124,6164,6168,6224,6238,6265,6293,6297,6302,6318,6323,6330,6340,6345,6355,6365,6370,6385,6395,6413,6418,6426,6430,6435,6442,6447,6465,6470,6493,6509,6525,6529,6534,6551,6558,6590,6595,6600,6608,6619,6635,6642,6651,6664,6670,6679,6692,6698,6707,6720,6726,6731,6739,6753,6757,6766,6771,6786,6791,6795,6800,6804,6822,6847,6858,6863,6867,6883,6888,6892,6897],{"__ignoreMap":15},[177,6084,6085],{"class":179,"line":180},[177,6086,218],{"emptyLinePlaceholder":30},[177,6088,6089,6091,6094,6096,6099],{"class":179,"line":25},[177,6090,184],{"class":183},[177,6092,6093],{"class":187}," { ipcMain, safeStorage, utilityProcess } ",[177,6095,191],{"class":183},[177,6097,6098],{"class":194}," 'electron'",[177,6100,198],{"class":187},[177,6102,6103,6105,6108,6110,6113],{"class":179,"line":215},[177,6104,184],{"class":183},[177,6106,6107],{"class":187}," readline ",[177,6109,191],{"class":183},[177,6111,6112],{"class":194}," 'node:readline'",[177,6114,198],{"class":187},[177,6116,6117],{"class":179,"line":221},[177,6118,6119],{"class":2925},"// ...\n",[177,6121,6122],{"class":179,"line":250},[177,6123,218],{"emptyLinePlaceholder":30},[177,6125,6126,6128,6130,6133,6135,6138,6140,6143,6145,6148,6150,6153,6155,6157,6160,6162],{"class":179,"line":285},[177,6127,224],{"class":183},[177,6129,1410],{"class":183},[177,6131,6132],{"class":237}," setupVolumeBotBridge",[177,6134,241],{"class":187},[177,6136,6137],{"class":526},"mainWindow",[177,6139,530],{"class":183},[177,6141,6142],{"class":237}," BrowserWindow",[177,6144,536],{"class":187},[177,6146,6147],{"class":237},"getVault",[177,6149,530],{"class":183},[177,6151,6152],{"class":187}," () ",[177,6154,1908],{"class":183},[177,6156,544],{"class":230},[177,6158,6159],{"class":183}," |",[177,6161,1228],{"class":230},[177,6163,1200],{"class":187},[177,6165,6166],{"class":179,"line":308},[177,6167,5477],{"class":187},[177,6169,6170,6173,6176,6178,6181,6183,6185,6187,6190,6192,6194,6196,6199,6201,6203,6205,6208,6210,6213,6215,6218,6220,6222],{"class":179,"line":329},[177,6171,6172],{"class":187}," ipcMain.",[177,6174,6175],{"class":237},"handle",[177,6177,241],{"class":187},[177,6179,6180],{"class":194},"'trade:volume-bot'",[177,6182,536],{"class":187},[177,6184,2861],{"class":183},[177,6186,523],{"class":187},[177,6188,6189],{"class":526},"_event",[177,6191,536],{"class":187},[177,6193,3783],{"class":526},[177,6195,536],{"class":187},[177,6197,6198],{"class":526},"maxSolPerWallet",[177,6200,536],{"class":187},[177,6202,4217],{"class":526},[177,6204,536],{"class":187},[177,6206,6207],{"class":526},"interval",[177,6209,536],{"class":187},[177,6211,6212],{"class":526},"shouldMassOut",[177,6214,536],{"class":187},[177,6216,6217],{"class":526},"ttl",[177,6219,1905],{"class":187},[177,6221,1908],{"class":183},[177,6223,453],{"class":187},[177,6225,6226,6228,6231,6233,6236],{"class":179,"line":350},[177,6227,569],{"class":183},[177,6229,6230],{"class":230}," vault",[177,6232,234],{"class":183},[177,6234,6235],{"class":237}," getVault",[177,6237,717],{"class":187},[177,6239,6240,6242,6244,6246,6249,6252,6255,6257,6260,6263],{"class":179,"line":384},[177,6241,932],{"class":183},[177,6243,523],{"class":187},[177,6245,1741],{"class":183},[177,6247,6248],{"class":187},"vault) ",[177,6250,6251],{"class":183},"return",[177,6253,6254],{"class":187}," { ok: ",[177,6256,3802],{"class":230},[177,6258,6259],{"class":187},", reason: ",[177,6261,6262],{"class":194},"'UNAUTHORIZED'",[177,6264,501],{"class":187},[177,6266,6267,6269,6272,6274,6277,6280,6282,6284,6287,6290],{"class":179,"line":591},[177,6268,569],{"class":183},[177,6270,6271],{"class":230}," decryptedPassword",[177,6273,234],{"class":183},[177,6275,6276],{"class":187}," safeStorage.",[177,6278,6279],{"class":237},"decryptString",[177,6281,1148],{"class":187},[177,6283,191],{"class":237},[177,6285,6286],{"class":187},"(vault, ",[177,6288,6289],{"class":194},"'base64'",[177,6291,6292],{"class":187},"));  \n",[177,6294,6295],{"class":179,"line":613},[177,6296,218],{"emptyLinePlaceholder":30},[177,6298,6299],{"class":179,"line":641},[177,6300,6301],{"class":2925},"    //fork heavy worker\n",[177,6303,6304,6307,6309,6312,6315],{"class":179,"line":647},[177,6305,6306],{"class":187},"    workerThread ",[177,6308,984],{"class":183},[177,6310,6311],{"class":187}," utilityProcess.",[177,6313,6314],{"class":237},"fork",[177,6316,6317],{"class":187},"(cliScriptPath, [], {\n",[177,6319,6320],{"class":179,"line":670},[177,6321,6322],{"class":187},"        env: {\n",[177,6324,6325,6328],{"class":179,"line":701},[177,6326,6327],{"class":183},"          ...",[177,6329,3953],{"class":187},[177,6331,6332,6335,6338],{"class":179,"line":720},[177,6333,6334],{"class":187},"          SOLANA_BOTS_INTERNAL: ",[177,6336,6337],{"class":194},"'volume-worker'",[177,6339,464],{"class":187},[177,6341,6342],{"class":179,"line":725},[177,6343,6344],{"class":187},"          TARGET_MINT: targetMintAddress,\n",[177,6346,6347,6350,6352],{"class":179,"line":738},[177,6348,6349],{"class":187},"          AMOUNT: ",[177,6351,2949],{"class":237},[177,6353,6354],{"class":187},"(maxSolPerWallet),\n",[177,6356,6357,6360,6362],{"class":179,"line":750},[177,6358,6359],{"class":187},"          WALLETS: ",[177,6361,2949],{"class":237},[177,6363,6364],{"class":187},"(numberOfWallets),\n",[177,6366,6367],{"class":179,"line":787},[177,6368,6369],{"class":187},"          PASSWORD: decryptedPassword,\n",[177,6371,6372,6375,6377,6379,6381,6383],{"class":179,"line":793},[177,6373,6374],{"class":187},"          INTERVAL: ",[177,6376,2949],{"class":237},[177,6378,4475],{"class":187},[177,6380,3996],{"class":183},[177,6382,987],{"class":230},[177,6384,381],{"class":187},[177,6386,6387,6390,6392],{"class":179,"line":798},[177,6388,6389],{"class":187},"          MASS_OUT: ",[177,6391,2949],{"class":237},[177,6393,6394],{"class":187},"(shouldMassOut),\n",[177,6396,6397,6400,6402,6405,6408,6410],{"class":179,"line":803},[177,6398,6399],{"class":187},"          TTL: ttl ",[177,6401,3696],{"class":183},[177,6403,6404],{"class":237}," String",[177,6406,6407],{"class":187},"(ttl) ",[177,6409,530],{"class":183},[177,6411,6412],{"class":194}," \"\"\n",[177,6414,6415],{"class":179,"line":842},[177,6416,6417],{"class":187},"        },\n",[177,6419,6420,6423],{"class":179,"line":849},[177,6421,6422],{"class":187},"        stdio: ",[177,6424,6425],{"class":194},"'pipe'\n",[177,6427,6428],{"class":179,"line":854},[177,6429,1506],{"class":187},[177,6431,6432],{"class":179,"line":869},[177,6433,6434],{"class":187},"      \n",[177,6436,6437,6439],{"class":179,"line":885},[177,6438,932],{"class":183},[177,6440,6441],{"class":187}," (workerThread.stdout) {\n",[177,6443,6444],{"class":179,"line":900},[177,6445,6446],{"class":2925},"        // read the output\n",[177,6448,6449,6451,6454,6456,6459,6462],{"class":179,"line":924},[177,6450,1799],{"class":183},[177,6452,6453],{"class":230}," stdoutReader",[177,6455,234],{"class":183},[177,6457,6458],{"class":187}," readline.",[177,6460,6461],{"class":237},"createInterface",[177,6463,6464],{"class":187},"({ input: workerThread.stdout });\n",[177,6466,6467],{"class":179,"line":929},[177,6468,6469],{"class":187},"        \n",[177,6471,6472,6475,6477,6479,6482,6485,6487,6489,6491],{"class":179,"line":947},[177,6473,6474],{"class":187},"        stdoutReader.",[177,6476,4529],{"class":237},[177,6478,241],{"class":187},[177,6480,6481],{"class":194},"'line'",[177,6483,6484],{"class":187},", (",[177,6486,179],{"class":526},[177,6488,1905],{"class":187},[177,6490,1908],{"class":183},[177,6492,453],{"class":187},[177,6494,6495,6497,6500,6502,6505,6507],{"class":179,"line":964},[177,6496,5184],{"class":183},[177,6498,6499],{"class":230}," cleanLine",[177,6501,234],{"class":183},[177,6503,6504],{"class":187}," line.",[177,6506,1810],{"class":237},[177,6508,717],{"class":187},[177,6510,6511,6514,6516,6518,6521,6523],{"class":179,"line":970},[177,6512,6513],{"class":183},"          if",[177,6515,523],{"class":187},[177,6517,1741],{"class":183},[177,6519,6520],{"class":187},"cleanLine) ",[177,6522,6251],{"class":183},[177,6524,198],{"class":187},[177,6526,6527],{"class":179,"line":975},[177,6528,218],{"emptyLinePlaceholder":30},[177,6530,6531],{"class":179,"line":992},[177,6532,6533],{"class":2925},"          // metric token\n",[177,6535,6536,6538,6541,6544,6546,6549],{"class":179,"line":1016},[177,6537,6513],{"class":183},[177,6539,6540],{"class":187}," (cleanLine.",[177,6542,6543],{"class":237},"startsWith",[177,6545,241],{"class":187},[177,6547,6548],{"class":194},"'__BOT_METRIC__:'",[177,6550,1293],{"class":187},[177,6552,6553,6556],{"class":179,"line":1036},[177,6554,6555],{"class":183},"            try",[177,6557,453],{"class":187},[177,6559,6560,6562,6565,6567,6569,6571,6573,6576,6579,6581,6583,6585,6588],{"class":179,"line":1056},[177,6561,3010],{"class":183},[177,6563,6564],{"class":230}," event",[177,6566,234],{"class":183},[177,6568,1850],{"class":230},[177,6570,65],{"class":187},[177,6572,1855],{"class":237},[177,6574,6575],{"class":187},"(cleanLine.",[177,6577,6578],{"class":237},"replace",[177,6580,241],{"class":187},[177,6582,6548],{"class":194},[177,6584,536],{"class":187},[177,6586,6587],{"class":194},"''",[177,6589,1627],{"class":187},[177,6591,6592],{"class":179,"line":1073},[177,6593,6594],{"class":187},"              \n",[177,6596,6597],{"class":179,"line":1078},[177,6598,6599],{"class":2925},"              // Route the data to the frontend\n",[177,6601,6602,6605],{"class":179,"line":1101},[177,6603,6604],{"class":183},"              switch",[177,6606,6607],{"class":187}," (event.type) {\n",[177,6609,6610,6613,6616],{"class":179,"line":1122},[177,6611,6612],{"class":183},"                case",[177,6614,6615],{"class":194}," 'stats'",[177,6617,6618],{"class":187},":\n",[177,6620,6621,6624,6627,6629,6632],{"class":179,"line":1134},[177,6622,6623],{"class":187},"                  mainWindow.webContents.",[177,6625,6626],{"class":237},"send",[177,6628,241],{"class":187},[177,6630,6631],{"class":194},"'volume-bot:stats'",[177,6633,6634],{"class":187},", event.payload);\n",[177,6636,6637,6640],{"class":179,"line":1139},[177,6638,6639],{"class":183},"                  break",[177,6641,198],{"class":187},[177,6643,6644,6646,6649],{"class":179,"line":1156},[177,6645,6612],{"class":183},[177,6647,6648],{"class":194}," 'log'",[177,6650,6618],{"class":187},[177,6652,6653,6655,6657,6659,6662],{"class":179,"line":1167},[177,6654,6623],{"class":187},[177,6656,6626],{"class":237},[177,6658,241],{"class":187},[177,6660,6661],{"class":194},"'volume-bot:log'",[177,6663,6634],{"class":187},[177,6665,6666,6668],{"class":179,"line":1177},[177,6667,6639],{"class":183},[177,6669,198],{"class":187},[177,6671,6672,6674,6677],{"class":179,"line":1183},[177,6673,6612],{"class":183},[177,6675,6676],{"class":194}," 'activity'",[177,6678,6618],{"class":187},[177,6680,6681,6683,6685,6687,6690],{"class":179,"line":1203},[177,6682,6623],{"class":187},[177,6684,6626],{"class":237},[177,6686,241],{"class":187},[177,6688,6689],{"class":194},"'volume-bot:activity'",[177,6691,6634],{"class":187},[177,6693,6694,6696],{"class":179,"line":1233},[177,6695,6639],{"class":183},[177,6697,198],{"class":187},[177,6699,6700,6702,6705],{"class":179,"line":1260},[177,6701,6612],{"class":183},[177,6703,6704],{"class":194}," 'status'",[177,6706,6618],{"class":187},[177,6708,6709,6711,6713,6715,6718],{"class":179,"line":1296},[177,6710,6623],{"class":187},[177,6712,6626],{"class":237},[177,6714,241],{"class":187},[177,6716,6717],{"class":194},"'volume-bot:status'",[177,6719,6634],{"class":187},[177,6721,6722,6724],{"class":179,"line":1312},[177,6723,6639],{"class":183},[177,6725,198],{"class":187},[177,6727,6728],{"class":179,"line":1317},[177,6729,6730],{"class":187},"              }\n",[177,6732,6733,6735,6737],{"class":179,"line":1333},[177,6734,1945],{"class":187},[177,6736,744],{"class":183},[177,6738,747],{"class":187},[177,6740,6741,6744,6746,6748,6751],{"class":179,"line":1341},[177,6742,6743],{"class":187},"              console.",[177,6745,1192],{"class":237},[177,6747,241],{"class":187},[177,6749,6750],{"class":194},"'Failed to parse bot metric line:'",[177,6752,3594],{"class":187},[177,6754,6755],{"class":179,"line":1346},[177,6756,2009],{"class":187},[177,6758,6759,6762,6764],{"class":179,"line":5250},[177,6760,6761],{"class":187},"          } ",[177,6763,1948],{"class":183},[177,6765,453],{"class":187},[177,6767,6768],{"class":179,"line":5258},[177,6769,6770],{"class":2925},"             // pass standard console logs directly to the UI log viewer\n",[177,6772,6773,6776,6778,6780,6783],{"class":179,"line":5268},[177,6774,6775],{"class":187},"            mainWindow.webContents.",[177,6777,6626],{"class":237},[177,6779,241],{"class":187},[177,6781,6782],{"class":194},"'volume-bot:raw-console'",[177,6784,6785],{"class":187},", cleanLine);\n",[177,6787,6788],{"class":179,"line":5273},[177,6789,6790],{"class":187},"          }\n",[177,6792,6793],{"class":179,"line":5291},[177,6794,2121],{"class":187},[177,6796,6797],{"class":179,"line":5312},[177,6798,6799],{"class":187},"      }\n",[177,6801,6802],{"class":179,"line":5322},[177,6803,218],{"emptyLinePlaceholder":30},[177,6805,6806,6809,6811,6813,6816,6818,6820],{"class":179,"line":5332},[177,6807,6808],{"class":187},"      workerThread.",[177,6810,4529],{"class":237},[177,6812,241],{"class":187},[177,6814,6815],{"class":194},"'exit'",[177,6817,4537],{"class":187},[177,6819,1908],{"class":183},[177,6821,453],{"class":187},[177,6823,6824,6827,6829,6831,6833,6836,6839,6842,6844],{"class":179,"line":5338},[177,6825,6826],{"class":187},"        mainWindow.webContents.",[177,6828,6626],{"class":237},[177,6830,241],{"class":187},[177,6832,6717],{"class":194},[177,6834,6835],{"class":187},", { status: ",[177,6837,6838],{"class":194},"'stopped'",[177,6840,6841],{"class":187},", stoppedAt: Date.",[177,6843,4138],{"class":237},[177,6845,6846],{"class":187},"() });\n",[177,6848,6849,6852,6854,6856],{"class":179,"line":5346},[177,6850,6851],{"class":187},"        workerThread ",[177,6853,984],{"class":183},[177,6855,1228],{"class":230},[177,6857,198],{"class":187},[177,6859,6860],{"class":179,"line":5356},[177,6861,6862],{"class":187},"      });\n",[177,6864,6865],{"class":179,"line":5362},[177,6866,218],{"emptyLinePlaceholder":30},[177,6868,6869,6871,6873,6875,6878,6881],{"class":179,"line":5368},[177,6870,2712],{"class":183},[177,6872,6254],{"class":187},[177,6874,279],{"class":230},[177,6876,6877],{"class":187},", data: ",[177,6879,6880],{"class":194},"'SUCCESS'",[177,6882,501],{"class":187},[177,6884,6885],{"class":179,"line":5374},[177,6886,6887],{"class":187},"  });\n",[177,6889,6890],{"class":179,"line":5380},[177,6891,5477],{"class":187},[177,6893,6894],{"class":179,"line":5386},[177,6895,6896],{"class":2925},"  // stop logic\n",[177,6898,6899],{"class":179,"line":5398},[177,6900,1536],{"class":187},[17,6902,6903,6904,6907,6908,6911],{},"The Vue application then simply sets up ",[174,6905,6906],{},"ipcRenderer.on"," listeners for events like ",[174,6909,6910],{},"volume-bot:stats",". When the utility process completes a trade, it outputs a metric line. The main process intercepts it, strips the prefix, parses the JSON, and pushes it directly into the Vue component's state.",[17,6913,6914,6915,6918,6919,6922],{},"It results in a smooth desktop UI where the user can watch their P&L, successful trades, and active logs update in real time, while the heavy cryptographic signing and network retry loops run isolated in the background. Stopping the bot is equally safe, the frontend sends a ",[174,6916,6917],{},"trade:volume-bot:stop"," command, the main process calls ",[174,6920,6921],{},"workerThread.kill()",", and the worker performs its graceful shutdown logic before exiting.",[17,6924,6925,6926,6942,6943,65],{},"You may also notice the ",[174,6927,6928,6930,6933,6935,6938,6940],{"className":169,"language":172,"style":15},[177,6929,6147],{"class":237},[177,6931,6932],{"class":187},": () ",[177,6934,1908],{"class":183},[177,6936,6937],{"class":187}," string ",[177,6939,1869],{"class":183},[177,6941,1228],{"class":230}," function. Remember the decrypt and encrypt functions? They require the master password the user has defined (in this case when first opening the app) that it needs to \"provide his password every time you make sensitive actions\", well with Electron I completely eliminated that. When the app first starts, after the first onboarding, it asks the user for its password, and saves it in Electron's native ",[53,6944,6947],{"href":6945,"rel":6946},"https://www.electronjs.org/docs/latest/api/safe-storage",[57],[174,6948,6949],{},"safeStorage",[17,6951,6952,6954],{},[174,6953,6949],{}," is an API built directly into Electron that allows applications to store data securely by leveraging OS-level encryption (like Keychain on macOS, DPAPI on Windows, and Secret Service API on Linux).",[17,6956,6957,6958,6960],{},"Instead of forcing the user to type their master password every single time they want to buy, sell, or view a wallet's private key, the app asks for it once per session. It then encrypts that master password using ",[174,6959,6949],{}," and holds it in memory within the main process as a base64 string (the vault variable).",[17,6962,6963,6964,530],{},"Here is how that looks in ",[174,6965,3862],{},[167,6967,6969],{"className":169,"code":6968,"filename":3862,"language":172,"meta":15,"style":15},"import { ipcMain, safeStorage } from 'electron';\n\nlet vault: string | null = null;\n\n//  inside app.whenReady() \n\nipcMain.handle('save-password', (_event, password: string) => {\n    // verify the OS supports native encryption\n    if (!safeStorage.isEncryptionAvailable()) {\n        return { ok: false, reason: 'OS_NOT_SUPPORTED' };\n    }\n    \n    try {\n        // Encrypt the password\n        const encrypted = safeStorage.encryptString(password);\n        \n        // Keep it in memory for the duration of the session\n        vault = encrypted.toString('base64'); \n\n        return { ok: true, data: 'Password saved' };\n    } catch {\n        return { ok: false, reason: 'ENCRYPTION_FAILED' };\n    }\n});\n\n// A quick helper to let the UI know if the user is \"logged in\"\nipcMain.handle('password-status', () => vault ? true : false);\n",[174,6970,6971,6984,6988,7008,7012,7017,7021,7051,7056,7073,7089,7093,7097,7103,7108,7123,7127,7132,7150,7154,7169,7177,7192,7196,7200,7204,7209],{"__ignoreMap":15},[177,6972,6973,6975,6978,6980,6982],{"class":179,"line":180},[177,6974,184],{"class":183},[177,6976,6977],{"class":187}," { ipcMain, safeStorage } ",[177,6979,191],{"class":183},[177,6981,6098],{"class":194},[177,6983,198],{"class":187},[177,6985,6986],{"class":179,"line":25},[177,6987,218],{"emptyLinePlaceholder":30},[177,6989,6990,6992,6994,6996,6998,7000,7002,7004,7006],{"class":179,"line":215},[177,6991,4105],{"class":183},[177,6993,6230],{"class":187},[177,6995,530],{"class":183},[177,6997,544],{"class":230},[177,6999,6159],{"class":183},[177,7001,1228],{"class":230},[177,7003,234],{"class":183},[177,7005,1228],{"class":230},[177,7007,198],{"class":187},[177,7009,7010],{"class":179,"line":221},[177,7011,218],{"emptyLinePlaceholder":30},[177,7013,7014],{"class":179,"line":250},[177,7015,7016],{"class":2925},"//  inside app.whenReady() \n",[177,7018,7019],{"class":179,"line":285},[177,7020,218],{"emptyLinePlaceholder":30},[177,7022,7023,7026,7028,7030,7033,7035,7037,7039,7041,7043,7045,7047,7049],{"class":179,"line":308},[177,7024,7025],{"class":187},"ipcMain.",[177,7027,6175],{"class":237},[177,7029,241],{"class":187},[177,7031,7032],{"class":194},"'save-password'",[177,7034,6484],{"class":187},[177,7036,6189],{"class":526},[177,7038,536],{"class":187},[177,7040,539],{"class":526},[177,7042,530],{"class":183},[177,7044,544],{"class":230},[177,7046,1905],{"class":187},[177,7048,1908],{"class":183},[177,7050,453],{"class":187},[177,7052,7053],{"class":179,"line":329},[177,7054,7055],{"class":2925},"    // verify the OS supports native encryption\n",[177,7057,7058,7060,7062,7064,7067,7070],{"class":179,"line":350},[177,7059,932],{"class":183},[177,7061,523],{"class":187},[177,7063,1741],{"class":183},[177,7065,7066],{"class":187},"safeStorage.",[177,7068,7069],{"class":237},"isEncryptionAvailable",[177,7071,7072],{"class":187},"()) {\n",[177,7074,7075,7077,7079,7081,7083,7086],{"class":179,"line":384},[177,7076,2149],{"class":183},[177,7078,6254],{"class":187},[177,7080,3802],{"class":230},[177,7082,6259],{"class":187},[177,7084,7085],{"class":194},"'OS_NOT_SUPPORTED'",[177,7087,7088],{"class":187}," };\n",[177,7090,7091],{"class":179,"line":591},[177,7092,967],{"class":187},[177,7094,7095],{"class":179,"line":613},[177,7096,644],{"class":187},[177,7098,7099,7101],{"class":179,"line":641},[177,7100,1792],{"class":183},[177,7102,453],{"class":187},[177,7104,7105],{"class":179,"line":647},[177,7106,7107],{"class":2925},"        // Encrypt the password\n",[177,7109,7110,7112,7114,7116,7118,7121],{"class":179,"line":670},[177,7111,1799],{"class":183},[177,7113,675],{"class":230},[177,7115,234],{"class":183},[177,7117,6276],{"class":187},[177,7119,7120],{"class":237},"encryptString",[177,7122,4632],{"class":187},[177,7124,7125],{"class":179,"line":701},[177,7126,6469],{"class":187},[177,7128,7129],{"class":179,"line":720},[177,7130,7131],{"class":2925},"        // Keep it in memory for the duration of the session\n",[177,7133,7134,7137,7139,7141,7143,7145,7147],{"class":179,"line":725},[177,7135,7136],{"class":187},"        vault ",[177,7138,984],{"class":183},[177,7140,1001],{"class":187},[177,7142,3116],{"class":237},[177,7144,241],{"class":187},[177,7146,6289],{"class":194},[177,7148,7149],{"class":187},"); \n",[177,7151,7152],{"class":179,"line":738},[177,7153,218],{"emptyLinePlaceholder":30},[177,7155,7156,7158,7160,7162,7164,7167],{"class":179,"line":750},[177,7157,2149],{"class":183},[177,7159,6254],{"class":187},[177,7161,279],{"class":230},[177,7163,6877],{"class":187},[177,7165,7166],{"class":194},"'Password saved'",[177,7168,7088],{"class":187},[177,7170,7171,7173,7175],{"class":179,"line":787},[177,7172,2156],{"class":187},[177,7174,744],{"class":183},[177,7176,453],{"class":187},[177,7178,7179,7181,7183,7185,7187,7190],{"class":179,"line":793},[177,7180,2149],{"class":183},[177,7182,6254],{"class":187},[177,7184,3802],{"class":230},[177,7186,6259],{"class":187},[177,7188,7189],{"class":194},"'ENCRYPTION_FAILED'",[177,7191,7088],{"class":187},[177,7193,7194],{"class":179,"line":798},[177,7195,967],{"class":187},[177,7197,7198],{"class":179,"line":803},[177,7199,387],{"class":187},[177,7201,7202],{"class":179,"line":842},[177,7203,218],{"emptyLinePlaceholder":30},[177,7205,7206],{"class":179,"line":849},[177,7207,7208],{"class":2925},"// A quick helper to let the UI know if the user is \"logged in\"\n",[177,7210,7211,7213,7215,7217,7220,7222,7224,7227,7229,7231,7233,7235],{"class":179,"line":854},[177,7212,7025],{"class":187},[177,7214,6175],{"class":237},[177,7216,241],{"class":187},[177,7218,7219],{"class":194},"'password-status'",[177,7221,4537],{"class":187},[177,7223,1908],{"class":183},[177,7225,7226],{"class":187}," vault ",[177,7228,3696],{"class":183},[177,7230,4551],{"class":230},[177,7232,3703],{"class":183},[177,7234,5617],{"class":230},[177,7236,588],{"class":187},[17,7238,7239],{},"This completely solves the UX problem of the CLI. When the user initiates a sensitive action (like mass selling or starting the volume bot), the main process simply decrypts the vault string back into the master password and passes it quietly to the background workers. The user gets a seamless \"one-click\" trading experience, while the underlying design remains completely encrypted at rest.",[404,7241,7243],{"id":7242},"the-operations-worker","The Operations Worker",[17,7245,7246],{},"With the volume bot isolated in its own utility process and the password securely stored, I noticed one final performance bottleneck. Even standard operations, like decrypting 50 wallets just to view their public addresses in the UI table, or parsing database positions, were causing slight UI stutters because they were running on the main Electron process.",[17,7248,7249],{},"Increasing that number to 100, and boom, the app crashed.",[17,7251,7252],{},"To fix this, I completely removed all database and heavy cryptographic logic from the main process. I spawned a second background utility process dedicated entirely to handling SQLite database reads/writes and standard trade executions.",[17,7254,7255,7256,7259],{},"I created a wrapper called ",[174,7257,7258],{},"routeOps"," that generates a unique UUID for each request, sends the action and the decrypted master password to the background worker, and waits for the specific response to come back:",[167,7261,7264],{"className":169,"code":7262,"filename":7263,"language":172,"meta":15,"style":15},"import { utilityProcess } from \"electron\";\nimport crypto from 'node:crypto';\n\n// ... (worker spawning logic) ...\n\nexport const routeOps = \u003CT>(action: string, decryptedPassword?: string, args: unknown[] = []) => {\n  return new Promise\u003CResultsWithId\u003CT>>((resolve) => {\n      const requestId = crypto.randomUUID();\n      const dbProcess = getDbProcess();\n\n      const handleResponse = (payload: unknown) => {    \n        const data = payload as ResultsWithId\u003CT>; \n        if (data.id !== requestId) return; // ignore messages not meant for this request\n        \n        dbProcess.off('message', handleResponse);\n        resolve(data);\n      };\n      \n    dbProcess.on('message', handleResponse);\n    \n    // send the work to the background process\n    dbProcess.postMessage({ \n      id: requestId,\n      action, \n      decryptedPassword, \n      args, \n    });\n  });\n};\n","spawnOpWorker.ts",[174,7265,7266,7280,7294,7298,7303,7307,7361,7390,7406,7420,7424,7449,7473,7495,7499,7515,7523,7528,7532,7545,7549,7554,7564,7569,7574,7579,7584,7588,7592],{"__ignoreMap":15},[177,7267,7268,7270,7273,7275,7278],{"class":179,"line":180},[177,7269,184],{"class":183},[177,7271,7272],{"class":187}," { utilityProcess } ",[177,7274,191],{"class":183},[177,7276,7277],{"class":194}," \"electron\"",[177,7279,198],{"class":187},[177,7281,7282,7284,7287,7289,7292],{"class":179,"line":25},[177,7283,184],{"class":183},[177,7285,7286],{"class":187}," crypto ",[177,7288,191],{"class":183},[177,7290,7291],{"class":194}," 'node:crypto'",[177,7293,198],{"class":187},[177,7295,7296],{"class":179,"line":215},[177,7297,218],{"emptyLinePlaceholder":30},[177,7299,7300],{"class":179,"line":221},[177,7301,7302],{"class":2925},"// ... (worker spawning logic) ...\n",[177,7304,7305],{"class":179,"line":250},[177,7306,218],{"emptyLinePlaceholder":30},[177,7308,7309,7311,7313,7316,7318,7320,7323,7326,7329,7331,7333,7335,7338,7340,7342,7344,7346,7348,7350,7352,7354,7357,7359],{"class":179,"line":285},[177,7310,224],{"class":183},[177,7312,227],{"class":183},[177,7314,7315],{"class":237}," routeOps",[177,7317,234],{"class":183},[177,7319,941],{"class":187},[177,7321,7322],{"class":237},"T",[177,7324,7325],{"class":187},">(",[177,7327,7328],{"class":526},"action",[177,7330,530],{"class":183},[177,7332,544],{"class":230},[177,7334,536],{"class":187},[177,7336,7337],{"class":526},"decryptedPassword",[177,7339,2529],{"class":183},[177,7341,544],{"class":230},[177,7343,536],{"class":187},[177,7345,5170],{"class":526},[177,7347,530],{"class":183},[177,7349,1197],{"class":230},[177,7351,1866],{"class":187},[177,7353,984],{"class":183},[177,7355,7356],{"class":187}," []) ",[177,7358,1908],{"class":183},[177,7360,453],{"class":187},[177,7362,7363,7366,7368,7370,7372,7375,7377,7379,7382,7384,7386,7388],{"class":179,"line":308},[177,7364,7365],{"class":183},"  return",[177,7367,756],{"class":183},[177,7369,2694],{"class":230},[177,7371,4143],{"class":187},[177,7373,7374],{"class":237},"ResultsWithId",[177,7376,4143],{"class":187},[177,7378,7322],{"class":237},[177,7380,7381],{"class":187},">>((",[177,7383,1696],{"class":526},[177,7385,1905],{"class":187},[177,7387,1908],{"class":183},[177,7389,453],{"class":187},[177,7391,7392,7394,7397,7399,7401,7404],{"class":179,"line":329},[177,7393,2591],{"class":183},[177,7395,7396],{"class":230}," requestId",[177,7398,234],{"class":183},[177,7400,577],{"class":187},[177,7402,7403],{"class":237},"randomUUID",[177,7405,717],{"class":187},[177,7407,7408,7410,7413,7415,7418],{"class":179,"line":350},[177,7409,2591],{"class":183},[177,7411,7412],{"class":230}," dbProcess",[177,7414,234],{"class":183},[177,7416,7417],{"class":237}," getDbProcess",[177,7419,717],{"class":187},[177,7421,7422],{"class":179,"line":384},[177,7423,218],{"emptyLinePlaceholder":30},[177,7425,7426,7428,7431,7433,7435,7438,7440,7442,7444,7446],{"class":179,"line":591},[177,7427,2591],{"class":183},[177,7429,7430],{"class":237}," handleResponse",[177,7432,234],{"class":183},[177,7434,523],{"class":187},[177,7436,7437],{"class":526},"payload",[177,7439,530],{"class":183},[177,7441,1197],{"class":230},[177,7443,1905],{"class":187},[177,7445,1908],{"class":183},[177,7447,7448],{"class":187}," {    \n",[177,7450,7451,7453,7456,7458,7461,7463,7466,7468,7470],{"class":179,"line":613},[177,7452,1799],{"class":183},[177,7454,7455],{"class":230}," data",[177,7457,234],{"class":183},[177,7459,7460],{"class":187}," payload ",[177,7462,1246],{"class":183},[177,7464,7465],{"class":237}," ResultsWithId",[177,7467,4143],{"class":187},[177,7469,7322],{"class":237},[177,7471,7472],{"class":187},">; \n",[177,7474,7475,7478,7481,7484,7487,7489,7492],{"class":179,"line":641},[177,7476,7477],{"class":183},"        if",[177,7479,7480],{"class":187}," (data.id ",[177,7482,7483],{"class":183},"!==",[177,7485,7486],{"class":187}," requestId) ",[177,7488,6251],{"class":183},[177,7490,7491],{"class":187},"; ",[177,7493,7494],{"class":2925},"// ignore messages not meant for this request\n",[177,7496,7497],{"class":179,"line":647},[177,7498,6469],{"class":187},[177,7500,7501,7504,7507,7509,7512],{"class":179,"line":670},[177,7502,7503],{"class":187},"        dbProcess.",[177,7505,7506],{"class":237},"off",[177,7508,241],{"class":187},[177,7510,7511],{"class":194},"'message'",[177,7513,7514],{"class":187},", handleResponse);\n",[177,7516,7517,7520],{"class":179,"line":701},[177,7518,7519],{"class":237},"        resolve",[177,7521,7522],{"class":187},"(data);\n",[177,7524,7525],{"class":179,"line":720},[177,7526,7527],{"class":187},"      };\n",[177,7529,7530],{"class":179,"line":725},[177,7531,6434],{"class":187},[177,7533,7534,7537,7539,7541,7543],{"class":179,"line":738},[177,7535,7536],{"class":187},"    dbProcess.",[177,7538,4529],{"class":237},[177,7540,241],{"class":187},[177,7542,7511],{"class":194},[177,7544,7514],{"class":187},[177,7546,7547],{"class":179,"line":750},[177,7548,644],{"class":187},[177,7550,7551],{"class":179,"line":787},[177,7552,7553],{"class":2925},"    // send the work to the background process\n",[177,7555,7556,7558,7561],{"class":179,"line":793},[177,7557,7536],{"class":187},[177,7559,7560],{"class":237},"postMessage",[177,7562,7563],{"class":187},"({ \n",[177,7565,7566],{"class":179,"line":798},[177,7567,7568],{"class":187},"      id: requestId,\n",[177,7570,7571],{"class":179,"line":803},[177,7572,7573],{"class":187},"      action, \n",[177,7575,7576],{"class":179,"line":842},[177,7577,7578],{"class":187},"      decryptedPassword, \n",[177,7580,7581],{"class":179,"line":849},[177,7582,7583],{"class":187},"      args, \n",[177,7585,7586],{"class":179,"line":854},[177,7587,1506],{"class":187},[177,7589,7590],{"class":179,"line":869},[177,7591,6887],{"class":187},[177,7593,7594],{"class":179,"line":885},[177,7595,501],{"class":187},[17,7597,7598,7599,7602,7603,7605],{},"Then, inside the ",[174,7600,7601],{},"OperationWorker.ts"," file, an if/else block intercepts these actions, runs the heavy ",[174,7604,433],{}," hashing to unlock the SQLite wallets, interacts with the blockchain, and sends the payload back:",[167,7607,7609],{"className":169,"code":7608,"filename":7601,"language":172,"meta":15,"style":15},"process.parentPort.on('message', async (event) => {\n      const { id, action, args, decryptedPassword } = event.data as WorkerRequest;\n      \n      \n      try {\n        let result;\n        if (action === 'get:wallets:all') {\n          result = await getAllWalletsView(decryptedPassword);\n        }\n\n        else if (action === 'get:wallets:management') {\n          result = await getWalletsManagementView(decryptedPassword);\n        } \n\n        else if (action === 'get:wallets:one') {\n          if (!args[0] || typeof args[0] !== 'string') {\n            throw new Error('Invalid arguments: Expected a string public key');\n          } else {\n            result = await getByPubkey(decryptedPassword, args[0]);\n          }\n        }   \n        // ... other actions\n\n     // catch all errors and normalize results\n    if (result !== null && typeof result === 'object' && 'ok' in result) {\n      if (!result.ok) {\n        throw new Error(result.reason ?? 'Underlying operation failed');\n      }\n      if ('data' in result) result = result.data;\n      else {\n        throw new Error(result.reason ?? 'Unexpected data type');\n      }\n    }\n      // send\n      process.parentPort.postMessage({ \n        id, \n        ok: true, \n        date: new Date().toISOString(), \n        data: result \n      });\n\n      } catch (err) {\n        // send caught error\n        const message = err instanceof Error ? err.message : String(err);\n        process.parentPort.postMessage({ \n            id, \n            ok: false, \n            date: new Date().toISOString(), \n            reason: message \n        });\n      }\n})\n",[174,7610,7611,7637,7674,7678,7682,7689,7696,7710,7725,7729,7733,7749,7762,7767,7771,7786,7818,7834,7842,7862,7866,7871,7876,7880,7885,7919,7931,7949,7953,7972,7979,7996,8000,8004,8009,8018,8023,8031,8050,8055,8059,8063,8072,8077,8104,8113,8118,8127,8142,8147,8151,8155],{"__ignoreMap":15},[177,7612,7613,7616,7618,7620,7622,7624,7626,7628,7631,7633,7635],{"class":179,"line":180},[177,7614,7615],{"class":187},"process.parentPort.",[177,7617,4529],{"class":237},[177,7619,241],{"class":187},[177,7621,7511],{"class":194},[177,7623,536],{"class":187},[177,7625,2861],{"class":183},[177,7627,523],{"class":187},[177,7629,7630],{"class":526},"event",[177,7632,1905],{"class":187},[177,7634,1908],{"class":183},[177,7636,453],{"class":187},[177,7638,7639,7641,7644,7647,7649,7651,7653,7655,7657,7659,7662,7664,7667,7669,7672],{"class":179,"line":25},[177,7640,2591],{"class":183},[177,7642,7643],{"class":187}," { ",[177,7645,7646],{"class":230},"id",[177,7648,536],{"class":187},[177,7650,7328],{"class":230},[177,7652,536],{"class":187},[177,7654,5170],{"class":230},[177,7656,536],{"class":187},[177,7658,7337],{"class":230},[177,7660,7661],{"class":187}," } ",[177,7663,984],{"class":183},[177,7665,7666],{"class":187}," event.data ",[177,7668,1246],{"class":183},[177,7670,7671],{"class":237}," WorkerRequest",[177,7673,198],{"class":187},[177,7675,7676],{"class":179,"line":215},[177,7677,6434],{"class":187},[177,7679,7680],{"class":179,"line":221},[177,7681,6434],{"class":187},[177,7683,7684,7687],{"class":179,"line":250},[177,7685,7686],{"class":183},"      try",[177,7688,453],{"class":187},[177,7690,7691,7693],{"class":179,"line":285},[177,7692,1817],{"class":183},[177,7694,7695],{"class":187}," result;\n",[177,7697,7698,7700,7703,7705,7708],{"class":179,"line":308},[177,7699,7477],{"class":183},[177,7701,7702],{"class":187}," (action ",[177,7704,1268],{"class":183},[177,7706,7707],{"class":194}," 'get:wallets:all'",[177,7709,1200],{"class":187},[177,7711,7712,7715,7717,7719,7722],{"class":179,"line":329},[177,7713,7714],{"class":187},"          result ",[177,7716,984],{"class":183},[177,7718,1600],{"class":183},[177,7720,7721],{"class":237}," getAllWalletsView",[177,7723,7724],{"class":187},"(decryptedPassword);\n",[177,7726,7727],{"class":179,"line":350},[177,7728,2041],{"class":187},[177,7730,7731],{"class":179,"line":384},[177,7732,218],{"emptyLinePlaceholder":30},[177,7734,7735,7738,7740,7742,7744,7747],{"class":179,"line":591},[177,7736,7737],{"class":183},"        else",[177,7739,1951],{"class":183},[177,7741,7702],{"class":187},[177,7743,1268],{"class":183},[177,7745,7746],{"class":194}," 'get:wallets:management'",[177,7748,1200],{"class":187},[177,7750,7751,7753,7755,7757,7760],{"class":179,"line":613},[177,7752,7714],{"class":187},[177,7754,984],{"class":183},[177,7756,1600],{"class":183},[177,7758,7759],{"class":237}," getWalletsManagementView",[177,7761,7724],{"class":187},[177,7763,7764],{"class":179,"line":641},[177,7765,7766],{"class":187},"        } \n",[177,7768,7769],{"class":179,"line":647},[177,7770,218],{"emptyLinePlaceholder":30},[177,7772,7773,7775,7777,7779,7781,7784],{"class":179,"line":670},[177,7774,7737],{"class":183},[177,7776,1951],{"class":183},[177,7778,7702],{"class":187},[177,7780,1268],{"class":183},[177,7782,7783],{"class":194}," 'get:wallets:one'",[177,7785,1200],{"class":187},[177,7787,7788,7790,7792,7794,7797,7799,7801,7803,7805,7808,7810,7812,7814,7816],{"class":179,"line":701},[177,7789,6513],{"class":183},[177,7791,523],{"class":187},[177,7793,1741],{"class":183},[177,7795,7796],{"class":187},"args[",[177,7798,5226],{"class":230},[177,7800,1595],{"class":187},[177,7802,1641],{"class":183},[177,7804,1911],{"class":183},[177,7806,7807],{"class":187}," args[",[177,7809,5226],{"class":230},[177,7811,1595],{"class":187},[177,7813,7483],{"class":183},[177,7815,1964],{"class":194},[177,7817,1200],{"class":187},[177,7819,7820,7823,7825,7827,7829,7832],{"class":179,"line":720},[177,7821,7822],{"class":183},"            throw",[177,7824,756],{"class":183},[177,7826,759],{"class":237},[177,7828,241],{"class":187},[177,7830,7831],{"class":194},"'Invalid arguments: Expected a string public key'",[177,7833,588],{"class":187},[177,7835,7836,7838,7840],{"class":179,"line":725},[177,7837,6761],{"class":187},[177,7839,1948],{"class":183},[177,7841,453],{"class":187},[177,7843,7844,7847,7849,7851,7854,7857,7859],{"class":179,"line":738},[177,7845,7846],{"class":187},"            result ",[177,7848,984],{"class":183},[177,7850,1600],{"class":183},[177,7852,7853],{"class":237}," getByPubkey",[177,7855,7856],{"class":187},"(decryptedPassword, args[",[177,7858,5226],{"class":230},[177,7860,7861],{"class":187},"]);\n",[177,7863,7864],{"class":179,"line":750},[177,7865,6790],{"class":187},[177,7867,7868],{"class":179,"line":787},[177,7869,7870],{"class":187},"        }   \n",[177,7872,7873],{"class":179,"line":793},[177,7874,7875],{"class":2925},"        // ... other actions\n",[177,7877,7878],{"class":179,"line":798},[177,7879,218],{"emptyLinePlaceholder":30},[177,7881,7882],{"class":179,"line":803},[177,7883,7884],{"class":2925},"     // catch all errors and normalize results\n",[177,7886,7887,7889,7892,7894,7896,7898,7900,7903,7905,7908,7910,7913,7916],{"class":179,"line":842},[177,7888,932],{"class":183},[177,7890,7891],{"class":187}," (result ",[177,7893,7483],{"class":183},[177,7895,1228],{"class":230},[177,7897,2572],{"class":183},[177,7899,1911],{"class":183},[177,7901,7902],{"class":187}," result ",[177,7904,1268],{"class":183},[177,7906,7907],{"class":194}," 'object'",[177,7909,2572],{"class":183},[177,7911,7912],{"class":194}," 'ok'",[177,7914,7915],{"class":183}," in",[177,7917,7918],{"class":187}," result) {\n",[177,7920,7921,7924,7926,7928],{"class":179,"line":849},[177,7922,7923],{"class":183},"      if",[177,7925,523],{"class":187},[177,7927,1741],{"class":183},[177,7929,7930],{"class":187},"result.ok) {\n",[177,7932,7933,7935,7937,7939,7942,7944,7947],{"class":179,"line":854},[177,7934,1663],{"class":183},[177,7936,756],{"class":183},[177,7938,759],{"class":237},[177,7940,7941],{"class":187},"(result.reason ",[177,7943,3996],{"class":183},[177,7945,7946],{"class":194}," 'Underlying operation failed'",[177,7948,588],{"class":187},[177,7950,7951],{"class":179,"line":869},[177,7952,6799],{"class":187},[177,7954,7955,7957,7959,7962,7964,7967,7969],{"class":179,"line":885},[177,7956,7923],{"class":183},[177,7958,523],{"class":187},[177,7960,7961],{"class":194},"'data'",[177,7963,7915],{"class":183},[177,7965,7966],{"class":187}," result) result ",[177,7968,984],{"class":183},[177,7970,7971],{"class":187}," result.data;\n",[177,7973,7974,7977],{"class":179,"line":900},[177,7975,7976],{"class":183},"      else",[177,7978,453],{"class":187},[177,7980,7981,7983,7985,7987,7989,7991,7994],{"class":179,"line":924},[177,7982,1663],{"class":183},[177,7984,756],{"class":183},[177,7986,759],{"class":237},[177,7988,7941],{"class":187},[177,7990,3996],{"class":183},[177,7992,7993],{"class":194}," 'Unexpected data type'",[177,7995,588],{"class":187},[177,7997,7998],{"class":179,"line":929},[177,7999,6799],{"class":187},[177,8001,8002],{"class":179,"line":947},[177,8003,967],{"class":187},[177,8005,8006],{"class":179,"line":964},[177,8007,8008],{"class":2925},"      // send\n",[177,8010,8011,8014,8016],{"class":179,"line":970},[177,8012,8013],{"class":187},"      process.parentPort.",[177,8015,7560],{"class":237},[177,8017,7563],{"class":187},[177,8019,8020],{"class":179,"line":975},[177,8021,8022],{"class":187},"        id, \n",[177,8024,8025,8027,8029],{"class":179,"line":992},[177,8026,3799],{"class":187},[177,8028,279],{"class":230},[177,8030,3805],{"class":187},[177,8032,8033,8036,8039,8042,8044,8047],{"class":179,"line":1016},[177,8034,8035],{"class":187},"        date: ",[177,8037,8038],{"class":183},"new",[177,8040,8041],{"class":237}," Date",[177,8043,367],{"class":187},[177,8045,8046],{"class":237},"toISOString",[177,8048,8049],{"class":187},"(), \n",[177,8051,8052],{"class":179,"line":1036},[177,8053,8054],{"class":187},"        data: result \n",[177,8056,8057],{"class":179,"line":1056},[177,8058,6862],{"class":187},[177,8060,8061],{"class":179,"line":1073},[177,8062,218],{"emptyLinePlaceholder":30},[177,8064,8065,8068,8070],{"class":179,"line":1078},[177,8066,8067],{"class":187},"      } ",[177,8069,744],{"class":183},[177,8071,747],{"class":187},[177,8073,8074],{"class":179,"line":1101},[177,8075,8076],{"class":2925},"        // send caught error\n",[177,8078,8079,8081,8084,8086,8089,8091,8093,8095,8098,8100,8102],{"class":179,"line":1122},[177,8080,1799],{"class":183},[177,8082,8083],{"class":230}," message",[177,8085,234],{"class":183},[177,8087,8088],{"class":187}," err ",[177,8090,1216],{"class":183},[177,8092,759],{"class":237},[177,8094,1221],{"class":183},[177,8096,8097],{"class":187}," err.message ",[177,8099,530],{"class":183},[177,8101,6404],{"class":237},[177,8103,2182],{"class":187},[177,8105,8106,8109,8111],{"class":179,"line":1134},[177,8107,8108],{"class":187},"        process.parentPort.",[177,8110,7560],{"class":237},[177,8112,7563],{"class":187},[177,8114,8115],{"class":179,"line":1139},[177,8116,8117],{"class":187},"            id, \n",[177,8119,8120,8123,8125],{"class":179,"line":1156},[177,8121,8122],{"class":187},"            ok: ",[177,8124,3802],{"class":230},[177,8126,3805],{"class":187},[177,8128,8129,8132,8134,8136,8138,8140],{"class":179,"line":1167},[177,8130,8131],{"class":187},"            date: ",[177,8133,8038],{"class":183},[177,8135,8041],{"class":237},[177,8137,367],{"class":187},[177,8139,8046],{"class":237},[177,8141,8049],{"class":187},[177,8143,8144],{"class":179,"line":1177},[177,8145,8146],{"class":187},"            reason: message \n",[177,8148,8149],{"class":179,"line":1183},[177,8150,2121],{"class":187},[177,8152,8153],{"class":179,"line":1203},[177,8154,6799],{"class":187},[177,8156,8157],{"class":179,"line":1233},[177,8158,5919],{"class":187},[404,8160,8162],{"id":8161},"the-build-process","The Build Process",[17,8164,8165],{},"What started as a \"simple script to trade meme coins\" quickly grew into a deep dive into cryptography, state management, exponential backoffs, IPC bridges, and multi-process architecture.",[17,8167,8168],{},"Building a local-first application on Solana that handles high-concurrency mass operations securely is what I discovered a complex challenge. You cannot rely strictly on the blockchain RPCs for state when dealing with mass operations, and you cannot run heavy cryptography on a UI thread.",[17,8170,8171,8172,8174,8175,8177],{},"Using SQLite for local state tracking, ",[174,8173,429],{}," for wallet encryption, Electron's ",[174,8176,6949],{}," for session memory, and detached workers for execution, the resulting application ended up, resilient to rate limits, and safe enough for non-technical users.",[17,8179,8180],{},"With that said, when it was time to finally package all of this with Electron Forge, it very quickly became a massive headache.",[404,8182,8184],{"id":8183},"multi-process-vite-the-asar-archive","Multi-Process Vite & The ASAR Archive",[17,8186,8187,8188,8195],{},"Because I was using multiple detached utility processes (one for the Volume Bot, one for the Database Operations), each worker needed its own isolated entry point in the final compiled ",[53,8189,8192],{"href":8190,"rel":8191},"https://www.electronjs.org/docs/latest/tutorial/asar-archives",[57],[174,8193,8194],{},"app.asar"," archive.",[17,8197,8198],{},"Vite is great for frontends, but handling multiple Node.js backend scripts required creating completely separate configurations (vite.opworker.config.ts, vite.worker.config.ts). I had to explicitly configure Vite to bundle them for a Node environment by using SSR flags and preventing externalization:",[167,8200,8203],{"className":169,"code":8201,"filename":8202,"language":172,"meta":15,"style":15},"import { ignoreBunPlugin } from './vite.main.config';\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n        resolve: {\n        alias: {\n          '@solana-bots': path.resolve(__dirname, '../src'),\n          '@': path.resolve(__dirname, 'src'),\n        },\n      },\n  ssr: {\n    noExternal: true // Bundle everything\n  },\n    plugins: [ignoreBunPlugin],\n    optimizeDeps: {\n    exclude: ['bun:sqlite', 'drizzle-orm/bun-sqlite', 'drizzle-orm/bun-sqlite/migrator'] // vite still trys to optimize these even if they are external\n  },\n  build: {\n    outDir: 'dist-worker/ops',\n    ssr: true,\n    target: 'node20',\n    lib: {\n      entry: 'src/electron/utils/OperationWorker.ts',\n      formats: ['es'],\n    },\n    rollupOptions: {\n      external: [\n        'electron', \n        'better-sqlite3', // ignore native dependencies\n        'bun:sqlite',\n        'drizzle-orm/bun-sqlite', // ignore the cli adapters\n        'drizzle-orm/bun-sqlite/migrator',\n      ],\n      output: {\n        entryFileNames: '[name].mjs',\n        inlineDynamicImports: true,\n      },\n    },\n  },\n});\n","vite.opworker.config.ts",[174,8204,8205,8219,8250,8254,8266,8271,8276,8294,8310,8314,8319,8324,8334,8338,8343,8348,8371,8375,8380,8390,8399,8409,8414,8424,8435,8439,8444,8449,8456,8466,8473,8483,8490,8495,8500,8510,8519,8523,8527,8531],{"__ignoreMap":15},[177,8206,8207,8209,8212,8214,8217],{"class":179,"line":180},[177,8208,184],{"class":183},[177,8210,8211],{"class":187}," { ignoreBunPlugin } ",[177,8213,191],{"class":183},[177,8215,8216],{"class":194}," './vite.main.config'",[177,8218,198],{"class":187},[177,8220,8221,8223,8226,8228,8230,8233,8235,8238,8240,8242,8244,8247],{"class":179,"line":25},[177,8222,445],{"class":183},[177,8224,8225],{"class":230}," __dirname",[177,8227,234],{"class":183},[177,8229,1693],{"class":187},[177,8231,8232],{"class":237},"dirname",[177,8234,241],{"class":187},[177,8236,8237],{"class":237},"fileURLToPath",[177,8239,241],{"class":187},[177,8241,184],{"class":183},[177,8243,65],{"class":187},[177,8245,8246],{"class":230},"meta",[177,8248,8249],{"class":187},".url));\n",[177,8251,8252],{"class":179,"line":215},[177,8253,218],{"emptyLinePlaceholder":30},[177,8255,8256,8258,8261,8264],{"class":179,"line":221},[177,8257,224],{"class":183},[177,8259,8260],{"class":183}," default",[177,8262,8263],{"class":237}," defineConfig",[177,8265,1478],{"class":187},[177,8267,8268],{"class":179,"line":250},[177,8269,8270],{"class":187},"        resolve: {\n",[177,8272,8273],{"class":179,"line":285},[177,8274,8275],{"class":187},"        alias: {\n",[177,8277,8278,8281,8284,8286,8289,8292],{"class":179,"line":308},[177,8279,8280],{"class":194},"          '@solana-bots'",[177,8282,8283],{"class":187},": path.",[177,8285,1696],{"class":237},[177,8287,8288],{"class":187},"(__dirname, ",[177,8290,8291],{"class":194},"'../src'",[177,8293,381],{"class":187},[177,8295,8296,8299,8301,8303,8305,8308],{"class":179,"line":329},[177,8297,8298],{"class":194},"          '@'",[177,8300,8283],{"class":187},[177,8302,1696],{"class":237},[177,8304,8288],{"class":187},[177,8306,8307],{"class":194},"'src'",[177,8309,381],{"class":187},[177,8311,8312],{"class":179,"line":350},[177,8313,6417],{"class":187},[177,8315,8316],{"class":179,"line":384},[177,8317,8318],{"class":187},"      },\n",[177,8320,8321],{"class":179,"line":591},[177,8322,8323],{"class":187},"  ssr: {\n",[177,8325,8326,8329,8331],{"class":179,"line":613},[177,8327,8328],{"class":187},"    noExternal: ",[177,8330,279],{"class":230},[177,8332,8333],{"class":2925}," // Bundle everything\n",[177,8335,8336],{"class":179,"line":641},[177,8337,4864],{"class":187},[177,8339,8340],{"class":179,"line":647},[177,8341,8342],{"class":187},"    plugins: [ignoreBunPlugin],\n",[177,8344,8345],{"class":179,"line":670},[177,8346,8347],{"class":187},"    optimizeDeps: {\n",[177,8349,8350,8353,8356,8358,8361,8363,8366,8368],{"class":179,"line":701},[177,8351,8352],{"class":187},"    exclude: [",[177,8354,8355],{"class":194},"'bun:sqlite'",[177,8357,536],{"class":187},[177,8359,8360],{"class":194},"'drizzle-orm/bun-sqlite'",[177,8362,536],{"class":187},[177,8364,8365],{"class":194},"'drizzle-orm/bun-sqlite/migrator'",[177,8367,1595],{"class":187},[177,8369,8370],{"class":2925},"// vite still trys to optimize these even if they are external\n",[177,8372,8373],{"class":179,"line":720},[177,8374,4864],{"class":187},[177,8376,8377],{"class":179,"line":725},[177,8378,8379],{"class":187},"  build: {\n",[177,8381,8382,8385,8388],{"class":179,"line":738},[177,8383,8384],{"class":187},"    outDir: ",[177,8386,8387],{"class":194},"'dist-worker/ops'",[177,8389,464],{"class":187},[177,8391,8392,8395,8397],{"class":179,"line":750},[177,8393,8394],{"class":187},"    ssr: ",[177,8396,279],{"class":230},[177,8398,464],{"class":187},[177,8400,8401,8404,8407],{"class":179,"line":787},[177,8402,8403],{"class":187},"    target: ",[177,8405,8406],{"class":194},"'node20'",[177,8408,464],{"class":187},[177,8410,8411],{"class":179,"line":793},[177,8412,8413],{"class":187},"    lib: {\n",[177,8415,8416,8419,8422],{"class":179,"line":798},[177,8417,8418],{"class":187},"      entry: ",[177,8420,8421],{"class":194},"'src/electron/utils/OperationWorker.ts'",[177,8423,464],{"class":187},[177,8425,8426,8429,8432],{"class":179,"line":803},[177,8427,8428],{"class":187},"      formats: [",[177,8430,8431],{"class":194},"'es'",[177,8433,8434],{"class":187},"],\n",[177,8436,8437],{"class":179,"line":842},[177,8438,4006],{"class":187},[177,8440,8441],{"class":179,"line":849},[177,8442,8443],{"class":187},"    rollupOptions: {\n",[177,8445,8446],{"class":179,"line":854},[177,8447,8448],{"class":187},"      external: [\n",[177,8450,8451,8454],{"class":179,"line":869},[177,8452,8453],{"class":194},"        'electron'",[177,8455,3805],{"class":187},[177,8457,8458,8461,8463],{"class":179,"line":885},[177,8459,8460],{"class":194},"        'better-sqlite3'",[177,8462,536],{"class":187},[177,8464,8465],{"class":2925},"// ignore native dependencies\n",[177,8467,8468,8471],{"class":179,"line":900},[177,8469,8470],{"class":194},"        'bun:sqlite'",[177,8472,464],{"class":187},[177,8474,8475,8478,8480],{"class":179,"line":924},[177,8476,8477],{"class":194},"        'drizzle-orm/bun-sqlite'",[177,8479,536],{"class":187},[177,8481,8482],{"class":2925},"// ignore the cli adapters\n",[177,8484,8485,8488],{"class":179,"line":929},[177,8486,8487],{"class":194},"        'drizzle-orm/bun-sqlite/migrator'",[177,8489,464],{"class":187},[177,8491,8492],{"class":179,"line":947},[177,8493,8494],{"class":187},"      ],\n",[177,8496,8497],{"class":179,"line":964},[177,8498,8499],{"class":187},"      output: {\n",[177,8501,8502,8505,8508],{"class":179,"line":970},[177,8503,8504],{"class":187},"        entryFileNames: ",[177,8506,8507],{"class":194},"'[name].mjs'",[177,8509,464],{"class":187},[177,8511,8512,8515,8517],{"class":179,"line":975},[177,8513,8514],{"class":187},"        inlineDynamicImports: ",[177,8516,279],{"class":230},[177,8518,464],{"class":187},[177,8520,8521],{"class":179,"line":992},[177,8522,8318],{"class":187},[177,8524,8525],{"class":179,"line":1016},[177,8526,4006],{"class":187},[177,8528,8529],{"class":179,"line":1036},[177,8530,4864],{"class":187},[177,8532,8533],{"class":179,"line":1056},[177,8534,387],{"class":187},[17,8536,8537],{},"Both workers' configs look identical; what's different are the paths and entry points.",[17,8539,2432,8540,8543,8544,8547],{},[174,8541,8542],{},"ignoreBunPlugin"," plugin is a vite plugin defined in ",[174,8545,8546],{},"vite.main.config.ts"," to FORCE Vite to ignore bun:",[167,8549,8551],{"className":169,"code":8550,"filename":8546,"language":172,"meta":15,"style":15},"export const ignoreBunPlugin = {\n  name: 'ignore-bun-modules',\n  resolveId(id: string) {\n    if (id === 'bun:sqlite' || id.includes('bun-sqlite')) {\n      return { id, external: true };\n    }\n  }\n};\n",[174,8552,8553,8566,8576,8591,8617,8628,8632,8636],{"__ignoreMap":15},[177,8554,8555,8557,8559,8562,8564],{"class":179,"line":180},[177,8556,224],{"class":183},[177,8558,227],{"class":183},[177,8560,8561],{"class":230}," ignoreBunPlugin",[177,8563,234],{"class":183},[177,8565,453],{"class":187},[177,8567,8568,8571,8574],{"class":179,"line":25},[177,8569,8570],{"class":187},"  name: ",[177,8572,8573],{"class":194},"'ignore-bun-modules'",[177,8575,464],{"class":187},[177,8577,8578,8581,8583,8585,8587,8589],{"class":179,"line":215},[177,8579,8580],{"class":237},"  resolveId",[177,8582,241],{"class":187},[177,8584,7646],{"class":526},[177,8586,530],{"class":183},[177,8588,544],{"class":230},[177,8590,1200],{"class":187},[177,8592,8593,8595,8598,8600,8603,8605,8608,8610,8612,8615],{"class":179,"line":221},[177,8594,932],{"class":183},[177,8596,8597],{"class":187}," (id ",[177,8599,1268],{"class":183},[177,8601,8602],{"class":194}," 'bun:sqlite'",[177,8604,1274],{"class":183},[177,8606,8607],{"class":187}," id.",[177,8609,1285],{"class":237},[177,8611,241],{"class":187},[177,8613,8614],{"class":194},"'bun-sqlite'",[177,8616,1293],{"class":187},[177,8618,8619,8621,8624,8626],{"class":179,"line":250},[177,8620,2712],{"class":183},[177,8622,8623],{"class":187}," { id, external: ",[177,8625,279],{"class":230},[177,8627,7088],{"class":187},[177,8629,8630],{"class":179,"line":285},[177,8631,967],{"class":187},[177,8633,8634],{"class":179,"line":308},[177,8635,790],{"class":187},[177,8637,8638],{"class":179,"line":329},[177,8639,501],{"class":187},[17,8641,8642,8643,8646,8647,8650,8651,8653],{},"Furthermore, Electron's default ",[174,8644,8645],{},"electron-squirrel-startup"," module was crashing Vite's ESM build completely. I ended up having to write a custom ESM implementation of the startup script (",[174,8648,8649],{},"electronStartEsm.ts",") using Node's ",[174,8652,3857],{}," just to handle Windows shortcut creation properly without breaking the Vite pipeline.",[404,8655,8657],{"id":8656},"the-native-dependency","The Native Dependency",[17,8659,8660],{},"The biggest headache by far was SQLite.",[17,8662,8663],{},"For the CLI version, I used bun:sqlite because it is fast, built-in, and because compiling the CLI with better-sqlite3 using Bun would outright crash the binary. But Electron requires a Node-compatible native module, so for the desktop app, I had to use better-sqlite3. (While I could have used the Node adapter for everything, it would have required rewiring some of the queries and relying on a beta version of Drizzle ORM).",[17,8665,8666],{},"This requirement forced a dual-adapter pattern in the database connection file that checks the environment at runtime:",[167,8668,8671],{"className":169,"code":8669,"filename":8670,"language":172,"meta":15,"style":15},"const isBun = (typeof process.versions !== 'undefined' && process.versions.bun !== undefined) || process.env['CLI_BUILD'];\nlet db: AppDatabase;\n\nif (isBun) {\n  const { Database } = await import(/* @vite-ignore */ \"bun:sqlite\");\n  // ... bun setup\n} else {\n  const Database = (await import('better-sqlite3')).default;\n  // better-sqlite3 setup\n}\n","connection.ts",[174,8672,8673,8716,8730,8734,8741,8769,8774,8783,8806,8811],{"__ignoreMap":15},[177,8674,8675,8677,8680,8682,8684,8686,8689,8691,8694,8696,8699,8701,8703,8705,8707,8710,8713],{"class":179,"line":180},[177,8676,445],{"class":183},[177,8678,8679],{"class":230}," isBun",[177,8681,234],{"class":183},[177,8683,523],{"class":187},[177,8685,1956],{"class":183},[177,8687,8688],{"class":187}," process.versions ",[177,8690,7483],{"class":183},[177,8692,8693],{"class":194}," 'undefined'",[177,8695,2572],{"class":183},[177,8697,8698],{"class":187}," process.versions.bun ",[177,8700,7483],{"class":183},[177,8702,5644],{"class":230},[177,8704,1905],{"class":187},[177,8706,1641],{"class":183},[177,8708,8709],{"class":187}," process.env[",[177,8711,8712],{"class":194},"'CLI_BUILD'",[177,8714,8715],{"class":187},"];\n",[177,8717,8718,8720,8723,8725,8728],{"class":179,"line":25},[177,8719,4105],{"class":183},[177,8721,8722],{"class":187}," db",[177,8724,530],{"class":183},[177,8726,8727],{"class":237}," AppDatabase",[177,8729,198],{"class":187},[177,8731,8732],{"class":179,"line":215},[177,8733,218],{"emptyLinePlaceholder":30},[177,8735,8736,8738],{"class":179,"line":221},[177,8737,3468],{"class":183},[177,8739,8740],{"class":187}," (isBun) {\n",[177,8742,8743,8745,8747,8750,8752,8754,8756,8759,8761,8764,8767],{"class":179,"line":250},[177,8744,3671],{"class":183},[177,8746,7643],{"class":187},[177,8748,8749],{"class":230},"Database",[177,8751,7661],{"class":187},[177,8753,984],{"class":183},[177,8755,1600],{"class":183},[177,8757,8758],{"class":237}," import",[177,8760,241],{"class":187},[177,8762,8763],{"class":2925},"/* @vite-ignore */",[177,8765,8766],{"class":194}," \"bun:sqlite\"",[177,8768,588],{"class":187},[177,8770,8771],{"class":179,"line":285},[177,8772,8773],{"class":2925},"  // ... bun setup\n",[177,8775,8776,8779,8781],{"class":179,"line":308},[177,8777,8778],{"class":187},"} ",[177,8780,1948],{"class":183},[177,8782,453],{"class":187},[177,8784,8785,8787,8790,8792,8794,8796,8798,8800,8803],{"class":179,"line":329},[177,8786,3671],{"class":183},[177,8788,8789],{"class":230}," Database",[177,8791,234],{"class":183},[177,8793,523],{"class":187},[177,8795,2852],{"class":183},[177,8797,8758],{"class":183},[177,8799,241],{"class":187},[177,8801,8802],{"class":194},"'better-sqlite3'",[177,8804,8805],{"class":187},")).default;\n",[177,8807,8808],{"class":179,"line":350},[177,8809,8810],{"class":2925},"  // better-sqlite3 setup\n",[177,8812,8813],{"class":179,"line":384},[177,8814,1536],{"class":187},[17,8816,8817,8818,8821,8822,65],{},"This split immediately complicated database migrations. Because the CLI is compiled into a standalone binary and the UI is packaged into a read-only ASAR archive, the raw ",[174,8819,8820],{},".sql"," migration files generated by Drizzle had to be manually copied around during the build process and referenced differently depending on the environment. I had to write custom logic to locate the migrations folder dynamically—whether it was running in dev mode, from the compiled Bun executable directory, or from Electron's ",[174,8823,8824],{},"process.resourcesPath",[17,8826,8827,8828,8831,8832,8835],{},"This caused chaos during packaging. The Electron Forge Vite plugin couldn't properly detect and package the native C++ bindings for ",[174,8829,8830],{},"better-sqlite3",". It aggressively stripped node_modules during the ",[174,8833,8834],{},"ASAR"," creation, leaving the production app without a working database.",[17,8837,8838,8839,8844,8845,8848,8849,8851],{},"After checking ",[53,8840,8843],{"href":8841,"rel":8842},"https://github.com/electron/forge/issues",[57],"github issues",", I found the solution: you have to use a specific regex in the ",[174,8846,8847],{},"forge.config.ts"," ignore field to forcefully prevent Forge from discarding ",[174,8850,8830],{}," and its bindings:",[167,8853,8855],{"className":169,"code":8854,"filename":8847,"language":172,"meta":15,"style":15},"packagerConfig: {\n    name: 'Solana Bots',\n    asar: true,\n    ignore: [\n        // force electron Forge to KEEP better-sqlite3 and its bindings\n        /node_modules\\/(?!(better-sqlite3|bindings|file-uri-to-path)\\/)/,\n        /^\\/src($|\\/)/,\n        // ...\n    ]\n}\n",[174,8856,8857,8865,8876,8887,8895,8900,8935,8958,8963,8968],{"__ignoreMap":15},[177,8858,8859,8862],{"class":179,"line":180},[177,8860,8861],{"class":237},"packagerConfig",[177,8863,8864],{"class":187},": {\n",[177,8866,8867,8870,8872,8874],{"class":179,"line":25},[177,8868,8869],{"class":237},"    name",[177,8871,2976],{"class":187},[177,8873,5728],{"class":194},[177,8875,464],{"class":187},[177,8877,8878,8881,8883,8885],{"class":179,"line":215},[177,8879,8880],{"class":237},"    asar",[177,8882,2976],{"class":187},[177,8884,279],{"class":230},[177,8886,464],{"class":187},[177,8888,8889,8892],{"class":179,"line":221},[177,8890,8891],{"class":237},"    ignore",[177,8893,8894],{"class":187},": [\n",[177,8896,8897],{"class":179,"line":250},[177,8898,8899],{"class":2925},"        // force electron Forge to KEEP better-sqlite3 and its bindings\n",[177,8901,8902,8905,8909,8913,8916,8918,8921,8923,8926,8928,8930,8933],{"class":179,"line":285},[177,8903,8904],{"class":194},"        /",[177,8906,8908],{"class":8907},"sQeJH","node_modules",[177,8910,8912],{"class":8911},"sAxt1","\\/",[177,8914,8915],{"class":8907},"(?!(better-sqlite3",[177,8917,1869],{"class":183},[177,8919,8920],{"class":8907},"bindings",[177,8922,1869],{"class":183},[177,8924,8925],{"class":8907},"file-uri-to-path)",[177,8927,8912],{"class":8911},[177,8929,547],{"class":8907},[177,8931,8932],{"class":194},"/",[177,8934,464],{"class":187},[177,8936,8937,8939,8942,8944,8947,8950,8952,8954,8956],{"class":179,"line":308},[177,8938,8904],{"class":194},[177,8940,8941],{"class":183},"^",[177,8943,8912],{"class":8911},[177,8945,8946],{"class":8907},"src(",[177,8948,8949],{"class":183},"$|",[177,8951,8912],{"class":8911},[177,8953,547],{"class":8907},[177,8955,8932],{"class":194},[177,8957,464],{"class":187},[177,8959,8960],{"class":179,"line":329},[177,8961,8962],{"class":2925},"        // ...\n",[177,8964,8965],{"class":179,"line":350},[177,8966,8967],{"class":187},"    ]\n",[177,8969,8970],{"class":179,"line":384},[177,8971,1536],{"class":187},[17,8973,8974,8975,8977,8978,8980,8981,8984,8985,8988,8989,65],{},"To make matters funnier, I found out that the latest versions of ",[174,8976,8830],{}," clash with Electron's internal Chromium/OpenGL APIs on certain operating systems, causing hard crashes. The fix for me was rolling back ",[174,8979,8830],{}," to version ",[174,8982,8983],{},"12.9.0"," and pinning Electron to version ",[174,8986,8987],{},"41.0.0"," instead of the latest ",[174,8990,8991],{},"42.2.0",[404,8993,8995],{"id":8994},"cicd-inno-setup-and-cloudflare-r2","CI/CD, Inno Setup, and Cloudflare R2",[17,8997,8998,8999,9001],{},"Because ",[174,9000,8830],{}," relies on native binaries, you can't build the Windows installer on a Mac/Linux or vice versa. It must be compiled on the target OS. I moved the entire build pipeline into GitHub Actions, starting up both an Ubuntu and Windows runner.",[17,9003,9004,9005,9010],{},"For Windows, Electron's default Maker (Squirrel) is extremely poor for UX, it doesn't let users choose install directories and feels crappy. So I deleted it and switched to ",[53,9006,9009],{"href":9007,"rel":9008},"https://jrsoftware.org/ishelp/",[57],"Inno Setup"," which I later discovered.",[17,9012,9013,9014,9017,9018,65],{},"I wanted the installer to include both the Electron Desktop App and the compiled CLI tool, alongside those migration files. And I wanted the CLI tool to automatically be added to the user's system ",[174,9015,9016],{},"PATH"," so they could just open their terminal and type ",[174,9019,9020],{},"solana-bots",[17,9022,9023,9024,9027,9028,9030],{},"But modifying the system registry is risky; an installer shouldn't just inject environment variables, it needs to clean up after itself when uninstalled. With some help from AI, I wrote a custom Pascal script inside ",[174,9025,9026],{},"installer.iss"," that safely adds the CLI to the ",[174,9029,9016],{},", broadcasts the change to the OS, and parses and removes only that specific path string during uninstallation:",[167,9032,9036],{"className":9033,"code":9034,"language":9035,"meta":15,"style":15},"language-pascal shiki shiki-themes github-light github-dark github-dark","\n[Files]\nSource: \"ui\\out\\Solana Bots-win32-x64\\*\"; DestDir: \"{app}\"; Flags: ignoreversion recursesubdirs createallsubdirs\nSource: \"build\\cli\\solana-bots-win.exe\"; DestDir: \"{app}\\cli\"; DestName: \"solana-bots.exe\"; Flags: ignoreversion\nSource: \"build\\migrations\\*\"; DestDir: \"{app}\\migrations\"; Flags: ignoreversion recursesubdirs\n\n[Code]\nconst\n    MY_HWND_BROADCAST = $FFFF;\n    MY_WM_SETTINGCHANGE = $001A;\n    MY_SMTO_ABORTIFHUNG = 2;\n\nfunction SendMessageTimeout(hWnd: LongInt; Msg: LongInt; wParam: LongInt; lParam: String; fuFlags: LongInt; uTimeout: LongInt; var lpdwResult: LongInt): LongInt;\n    external 'SendMessageTimeoutW@user32.dll stdcall';\n\nprocedure RefreshEnvironment;\nvar\n    Dummy: LongInt;\nbegin\n    SendMessageTimeout(MY_HWND_BROADCAST, MY_WM_SETTINGCHANGE, 0, 'Environment', MY_SMTO_ABORTIFHUNG, 5000, Dummy);\nend;\n\nprocedure AddToPath;\nvar\n    OldPath: String;\n    NewPath: String;\n    AppPath: String;\nbegin\n    AppPath := ExpandConstant('{app}\\cli'); \n    if RegQueryStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', OldPath) then\n    begin\n        if Pos(Lowercase(AppPath), Lowercase(OldPath)) = 0 then\n        begin\n            NewPath := OldPath + ';' + AppPath;\n            RegWriteExpandStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', NewPath);\n        end;\n    end;\nend;\n\nprocedure CurStepChanged(CurStep: TSetupStep);\nbegin\n    if CurStep = ssPostInstall then\n    begin\n        AddToPath();\n        RefreshEnvironment();\n    end;\nend;\n\nprocedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);\nvar\n    sCurrent: String;\n    sAppPath: String;\n    sCurLower: String;\n    sAppLower: String;\n    p, startIdx, endIdx: Integer;\nbegin\n    if CurUninstallStep = usPostUninstall then\n    begin\n        sAppPath := ExpandConstant('{app}\\cli'); \n        if RegQueryStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', sCurrent) then\n        begin\n            sCurLower := Lowercase(sCurrent);\n            sAppLower := Lowercase(sAppPath);\n            p := Pos(sAppLower, sCurLower);\n            \n            // Cleanly parse and remove the specific CLI path\n            while p > 0 do\n            begin\n                startIdx := p;\n                endIdx := p + Length(sAppLower) - 1;\n                if (startIdx > 1) and (sCurLower[startIdx-1] = ';') then\n                    startIdx := startIdx - 1;\n                if (endIdx \u003C Length(sCurLower)) and (sCurLower[endIdx+1] = ';') then\n                    endIdx := endIdx + 1;\n                Delete(sCurrent, startIdx, endIdx - startIdx + 1);\n                sCurLower := Lowercase(sCurrent);\n                p := Pos(sAppLower, sCurLower);\n            end;\n\n            // Clean up trailing/double semicolons\n            while Pos(';;', sCurrent) > 0 do\n                Delete(sCurrent, Pos(';;', sCurrent), 1);\n            while (Length(sCurrent) > 0) and (sCurrent[1] = ';') do\n                Delete(sCurrent, 1, 1);\n            while (Length(sCurrent) > 0) and (sCurrent[Length(sCurrent)] = ';') do\n                Delete(sCurrent, Length(sCurrent), 1);\n\n            if RegWriteExpandStringValue(HKLM, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path', sCurrent) then\n            begin\n                RefreshEnvironment();\n            end;\n        end;\n    end;\nend;\n","pascal",[174,9037,9038,9042,9047,9070,9085,9100,9104,9109,9114,9124,9134,9143,9147,9203,9213,9217,9227,9232,9241,9246,9266,9273,9277,9286,9290,9299,9308,9317,9321,9331,9352,9357,9369,9374,9385,9399,9406,9413,9419,9423,9433,9437,9446,9450,9455,9460,9466,9472,9476,9486,9490,9499,9508,9517,9526,9536,9540,9549,9553,9562,9579,9583,9588,9593,9598,9603,9608,9621,9626,9631,9640,9668,9677,9699,9708,9717,9722,9727,9734,9738,9743,9760,9774,9801,9814,9835,9845,9850,9868,9873,9879,9886,9893,9900],{"__ignoreMap":15},[177,9039,9040],{"class":179,"line":180},[177,9041,218],{"emptyLinePlaceholder":30},[177,9043,9044],{"class":179,"line":25},[177,9045,9046],{"class":187},"[Files]\n",[177,9048,9049,9052,9055,9058,9061,9064,9067],{"class":179,"line":215},[177,9050,9051],{"class":187},"Source: \"ui\\",[177,9053,9054],{"class":183},"out",[177,9056,9057],{"class":187},"\\Solana Bots-win32-x64\\*\"; DestDir: \"",[177,9059,9060],{"class":2925},"{app}",[177,9062,9063],{"class":187},"\"; ",[177,9065,9066],{"class":183},"Flags",[177,9068,9069],{"class":187},": ignoreversion recursesubdirs createallsubdirs\n",[177,9071,9072,9075,9077,9080,9082],{"class":179,"line":221},[177,9073,9074],{"class":187},"Source: \"build\\cli\\solana-bots-win.exe\"; DestDir: \"",[177,9076,9060],{"class":2925},[177,9078,9079],{"class":187},"\\cli\"; DestName: \"solana-bots.exe\"; ",[177,9081,9066],{"class":183},[177,9083,9084],{"class":187},": ignoreversion\n",[177,9086,9087,9090,9092,9095,9097],{"class":179,"line":250},[177,9088,9089],{"class":187},"Source: \"build\\migrations\\*\"; DestDir: \"",[177,9091,9060],{"class":2925},[177,9093,9094],{"class":187},"\\migrations\"; ",[177,9096,9066],{"class":183},[177,9098,9099],{"class":187},": ignoreversion recursesubdirs\n",[177,9101,9102],{"class":179,"line":285},[177,9103,218],{"emptyLinePlaceholder":30},[177,9105,9106],{"class":179,"line":308},[177,9107,9108],{"class":187},"[Code]\n",[177,9110,9111],{"class":179,"line":329},[177,9112,9113],{"class":183},"const\n",[177,9115,9116,9119,9122],{"class":179,"line":350},[177,9117,9118],{"class":187},"    MY_HWND_BROADCAST = ",[177,9120,9121],{"class":230},"$FFFF",[177,9123,198],{"class":187},[177,9125,9126,9129,9132],{"class":179,"line":384},[177,9127,9128],{"class":187},"    MY_WM_SETTINGCHANGE = ",[177,9130,9131],{"class":230},"$001A",[177,9133,198],{"class":187},[177,9135,9136,9139,9141],{"class":179,"line":591},[177,9137,9138],{"class":187},"    MY_SMTO_ABORTIFHUNG = ",[177,9140,2677],{"class":230},[177,9142,198],{"class":187},[177,9144,9145],{"class":179,"line":613},[177,9146,218],{"emptyLinePlaceholder":30},[177,9148,9149,9152,9155,9158,9161,9164,9166,9169,9171,9174,9176,9179,9181,9184,9186,9188,9191,9194,9196,9199,9201],{"class":179,"line":641},[177,9150,9151],{"class":183},"function",[177,9153,9154],{"class":237}," SendMessageTimeout",[177,9156,9157],{"class":187},"(hWnd: ",[177,9159,9160],{"class":183},"LongInt",[177,9162,9163],{"class":187},"; Msg: ",[177,9165,9160],{"class":183},[177,9167,9168],{"class":187},"; wParam: ",[177,9170,9160],{"class":183},[177,9172,9173],{"class":187},"; lParam: ",[177,9175,2949],{"class":183},[177,9177,9178],{"class":187},"; fuFlags: ",[177,9180,9160],{"class":183},[177,9182,9183],{"class":187},"; uTimeout: ",[177,9185,9160],{"class":183},[177,9187,7491],{"class":187},[177,9189,9190],{"class":183},"var",[177,9192,9193],{"class":187}," lpdwResult: ",[177,9195,9160],{"class":183},[177,9197,9198],{"class":187},"): ",[177,9200,9160],{"class":183},[177,9202,198],{"class":187},[177,9204,9205,9208,9211],{"class":179,"line":647},[177,9206,9207],{"class":183},"    external",[177,9209,9210],{"class":194}," 'SendMessageTimeoutW@user32.dll stdcall'",[177,9212,198],{"class":187},[177,9214,9215],{"class":179,"line":670},[177,9216,218],{"emptyLinePlaceholder":30},[177,9218,9219,9222,9225],{"class":179,"line":701},[177,9220,9221],{"class":183},"procedure",[177,9223,9224],{"class":237}," RefreshEnvironment",[177,9226,198],{"class":187},[177,9228,9229],{"class":179,"line":720},[177,9230,9231],{"class":183},"var\n",[177,9233,9234,9237,9239],{"class":179,"line":725},[177,9235,9236],{"class":187},"    Dummy: ",[177,9238,9160],{"class":183},[177,9240,198],{"class":187},[177,9242,9243],{"class":179,"line":738},[177,9244,9245],{"class":183},"begin\n",[177,9247,9248,9251,9253,9255,9258,9261,9263],{"class":179,"line":750},[177,9249,9250],{"class":187},"    SendMessageTimeout(MY_HWND_BROADCAST, MY_WM_SETTINGCHANGE, ",[177,9252,5226],{"class":230},[177,9254,536],{"class":187},[177,9256,9257],{"class":194},"'Environment'",[177,9259,9260],{"class":187},", MY_SMTO_ABORTIFHUNG, ",[177,9262,4233],{"class":230},[177,9264,9265],{"class":187},", Dummy);\n",[177,9267,9268,9271],{"class":179,"line":787},[177,9269,9270],{"class":183},"end",[177,9272,198],{"class":187},[177,9274,9275],{"class":179,"line":793},[177,9276,218],{"emptyLinePlaceholder":30},[177,9278,9279,9281,9284],{"class":179,"line":798},[177,9280,9221],{"class":183},[177,9282,9283],{"class":237}," AddToPath",[177,9285,198],{"class":187},[177,9287,9288],{"class":179,"line":803},[177,9289,9231],{"class":183},[177,9291,9292,9295,9297],{"class":179,"line":842},[177,9293,9294],{"class":187},"    OldPath: ",[177,9296,2949],{"class":183},[177,9298,198],{"class":187},[177,9300,9301,9304,9306],{"class":179,"line":849},[177,9302,9303],{"class":187},"    NewPath: ",[177,9305,2949],{"class":183},[177,9307,198],{"class":187},[177,9309,9310,9313,9315],{"class":179,"line":854},[177,9311,9312],{"class":187},"    AppPath: ",[177,9314,2949],{"class":183},[177,9316,198],{"class":187},[177,9318,9319],{"class":179,"line":869},[177,9320,9245],{"class":183},[177,9322,9323,9326,9329],{"class":179,"line":885},[177,9324,9325],{"class":187},"    AppPath := ExpandConstant(",[177,9327,9328],{"class":194},"'{app}\\cli'",[177,9330,7149],{"class":187},[177,9332,9333,9335,9338,9341,9343,9346,9349],{"class":179,"line":900},[177,9334,932],{"class":183},[177,9336,9337],{"class":187}," RegQueryStringValue(HKLM, ",[177,9339,9340],{"class":194},"'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment'",[177,9342,536],{"class":187},[177,9344,9345],{"class":194},"'Path'",[177,9347,9348],{"class":187},", OldPath) ",[177,9350,9351],{"class":183},"then\n",[177,9353,9354],{"class":179,"line":924},[177,9355,9356],{"class":183},"    begin\n",[177,9358,9359,9361,9364,9366],{"class":179,"line":929},[177,9360,7477],{"class":183},[177,9362,9363],{"class":187}," Pos(Lowercase(AppPath), Lowercase(OldPath)) = ",[177,9365,5226],{"class":230},[177,9367,9368],{"class":183}," then\n",[177,9370,9371],{"class":179,"line":947},[177,9372,9373],{"class":183},"        begin\n",[177,9375,9376,9379,9382],{"class":179,"line":964},[177,9377,9378],{"class":187},"            NewPath := OldPath + ",[177,9380,9381],{"class":194},"';'",[177,9383,9384],{"class":187}," + AppPath;\n",[177,9386,9387,9390,9392,9394,9396],{"class":179,"line":970},[177,9388,9389],{"class":187},"            RegWriteExpandStringValue(HKLM, ",[177,9391,9340],{"class":194},[177,9393,536],{"class":187},[177,9395,9345],{"class":194},[177,9397,9398],{"class":187},", NewPath);\n",[177,9400,9401,9404],{"class":179,"line":975},[177,9402,9403],{"class":183},"        end",[177,9405,198],{"class":187},[177,9407,9408,9411],{"class":179,"line":992},[177,9409,9410],{"class":183},"    end",[177,9412,198],{"class":187},[177,9414,9415,9417],{"class":179,"line":1016},[177,9416,9270],{"class":183},[177,9418,198],{"class":187},[177,9420,9421],{"class":179,"line":1036},[177,9422,218],{"emptyLinePlaceholder":30},[177,9424,9425,9427,9430],{"class":179,"line":1056},[177,9426,9221],{"class":183},[177,9428,9429],{"class":237}," CurStepChanged",[177,9431,9432],{"class":187},"(CurStep: TSetupStep);\n",[177,9434,9435],{"class":179,"line":1073},[177,9436,9245],{"class":183},[177,9438,9439,9441,9444],{"class":179,"line":1078},[177,9440,932],{"class":183},[177,9442,9443],{"class":187}," CurStep = ssPostInstall ",[177,9445,9351],{"class":183},[177,9447,9448],{"class":179,"line":1101},[177,9449,9356],{"class":183},[177,9451,9452],{"class":179,"line":1122},[177,9453,9454],{"class":187},"        AddToPath();\n",[177,9456,9457],{"class":179,"line":1134},[177,9458,9459],{"class":187},"        RefreshEnvironment();\n",[177,9461,9462,9464],{"class":179,"line":1139},[177,9463,9410],{"class":183},[177,9465,198],{"class":187},[177,9467,9468,9470],{"class":179,"line":1156},[177,9469,9270],{"class":183},[177,9471,198],{"class":187},[177,9473,9474],{"class":179,"line":1167},[177,9475,218],{"emptyLinePlaceholder":30},[177,9477,9478,9480,9483],{"class":179,"line":1177},[177,9479,9221],{"class":183},[177,9481,9482],{"class":237}," CurUninstallStepChanged",[177,9484,9485],{"class":187},"(CurUninstallStep: TUninstallStep);\n",[177,9487,9488],{"class":179,"line":1183},[177,9489,9231],{"class":183},[177,9491,9492,9495,9497],{"class":179,"line":1203},[177,9493,9494],{"class":187},"    sCurrent: ",[177,9496,2949],{"class":183},[177,9498,198],{"class":187},[177,9500,9501,9504,9506],{"class":179,"line":1233},[177,9502,9503],{"class":187},"    sAppPath: ",[177,9505,2949],{"class":183},[177,9507,198],{"class":187},[177,9509,9510,9513,9515],{"class":179,"line":1260},[177,9511,9512],{"class":187},"    sCurLower: ",[177,9514,2949],{"class":183},[177,9516,198],{"class":187},[177,9518,9519,9522,9524],{"class":179,"line":1296},[177,9520,9521],{"class":187},"    sAppLower: ",[177,9523,2949],{"class":183},[177,9525,198],{"class":187},[177,9527,9528,9531,9534],{"class":179,"line":1312},[177,9529,9530],{"class":187},"    p, startIdx, endIdx: ",[177,9532,9533],{"class":183},"Integer",[177,9535,198],{"class":187},[177,9537,9538],{"class":179,"line":1317},[177,9539,9245],{"class":183},[177,9541,9542,9544,9547],{"class":179,"line":1333},[177,9543,932],{"class":183},[177,9545,9546],{"class":187}," CurUninstallStep = usPostUninstall ",[177,9548,9351],{"class":183},[177,9550,9551],{"class":179,"line":1341},[177,9552,9356],{"class":183},[177,9554,9555,9558,9560],{"class":179,"line":1346},[177,9556,9557],{"class":187},"        sAppPath := ExpandConstant(",[177,9559,9328],{"class":194},[177,9561,7149],{"class":187},[177,9563,9564,9566,9568,9570,9572,9574,9577],{"class":179,"line":5250},[177,9565,7477],{"class":183},[177,9567,9337],{"class":187},[177,9569,9340],{"class":194},[177,9571,536],{"class":187},[177,9573,9345],{"class":194},[177,9575,9576],{"class":187},", sCurrent) ",[177,9578,9351],{"class":183},[177,9580,9581],{"class":179,"line":5258},[177,9582,9373],{"class":183},[177,9584,9585],{"class":179,"line":5268},[177,9586,9587],{"class":187},"            sCurLower := Lowercase(sCurrent);\n",[177,9589,9590],{"class":179,"line":5273},[177,9591,9592],{"class":187},"            sAppLower := Lowercase(sAppPath);\n",[177,9594,9595],{"class":179,"line":5291},[177,9596,9597],{"class":187},"            p := Pos(sAppLower, sCurLower);\n",[177,9599,9600],{"class":179,"line":5312},[177,9601,9602],{"class":187},"            \n",[177,9604,9605],{"class":179,"line":5322},[177,9606,9607],{"class":2925},"            // Cleanly parse and remove the specific CLI path\n",[177,9609,9610,9613,9616,9618],{"class":179,"line":5332},[177,9611,9612],{"class":183},"            while",[177,9614,9615],{"class":187}," p > ",[177,9617,5226],{"class":230},[177,9619,9620],{"class":183}," do\n",[177,9622,9623],{"class":179,"line":5338},[177,9624,9625],{"class":183},"            begin\n",[177,9627,9628],{"class":179,"line":5346},[177,9629,9630],{"class":187},"                startIdx := p;\n",[177,9632,9633,9636,9638],{"class":179,"line":5356},[177,9634,9635],{"class":187},"                endIdx := p + Length(sAppLower) - ",[177,9637,493],{"class":230},[177,9639,198],{"class":187},[177,9641,9642,9645,9648,9650,9652,9654,9657,9659,9662,9664,9666],{"class":179,"line":5362},[177,9643,9644],{"class":183},"                if",[177,9646,9647],{"class":187}," (startIdx > ",[177,9649,493],{"class":230},[177,9651,1905],{"class":187},[177,9653,3734],{"class":183},[177,9655,9656],{"class":187}," (sCurLower[startIdx-",[177,9658,493],{"class":230},[177,9660,9661],{"class":187},"] = ",[177,9663,9381],{"class":194},[177,9665,1905],{"class":187},[177,9667,9351],{"class":183},[177,9669,9670,9673,9675],{"class":179,"line":5368},[177,9671,9672],{"class":187},"                    startIdx := startIdx - ",[177,9674,493],{"class":230},[177,9676,198],{"class":187},[177,9678,9679,9681,9684,9686,9689,9691,9693,9695,9697],{"class":179,"line":5374},[177,9680,9644],{"class":183},[177,9682,9683],{"class":187}," (endIdx \u003C Length(sCurLower)) ",[177,9685,3734],{"class":183},[177,9687,9688],{"class":187}," (sCurLower[endIdx+",[177,9690,493],{"class":230},[177,9692,9661],{"class":187},[177,9694,9381],{"class":194},[177,9696,1905],{"class":187},[177,9698,9351],{"class":183},[177,9700,9701,9704,9706],{"class":179,"line":5380},[177,9702,9703],{"class":187},"                    endIdx := endIdx + ",[177,9705,493],{"class":230},[177,9707,198],{"class":187},[177,9709,9710,9713,9715],{"class":179,"line":5386},[177,9711,9712],{"class":187},"                Delete(sCurrent, startIdx, endIdx - startIdx + ",[177,9714,493],{"class":230},[177,9716,588],{"class":187},[177,9718,9719],{"class":179,"line":5398},[177,9720,9721],{"class":187},"                sCurLower := Lowercase(sCurrent);\n",[177,9723,9724],{"class":179,"line":5404},[177,9725,9726],{"class":187},"                p := Pos(sAppLower, sCurLower);\n",[177,9728,9729,9732],{"class":179,"line":5409},[177,9730,9731],{"class":183},"            end",[177,9733,198],{"class":187},[177,9735,9736],{"class":179,"line":5414},[177,9737,218],{"emptyLinePlaceholder":30},[177,9739,9740],{"class":179,"line":5424},[177,9741,9742],{"class":2925},"            // Clean up trailing/double semicolons\n",[177,9744,9745,9747,9750,9753,9756,9758],{"class":179,"line":5429},[177,9746,9612],{"class":183},[177,9748,9749],{"class":187}," Pos(",[177,9751,9752],{"class":194},"';;'",[177,9754,9755],{"class":187},", sCurrent) > ",[177,9757,5226],{"class":230},[177,9759,9620],{"class":183},[177,9761,9762,9765,9767,9770,9772],{"class":179,"line":5448},[177,9763,9764],{"class":187},"                Delete(sCurrent, Pos(",[177,9766,9752],{"class":194},[177,9768,9769],{"class":187},", sCurrent), ",[177,9771,493],{"class":230},[177,9773,588],{"class":187},[177,9775,9776,9778,9781,9783,9785,9787,9790,9792,9794,9796,9798],{"class":179,"line":5469},[177,9777,9612],{"class":183},[177,9779,9780],{"class":187}," (Length(sCurrent) > ",[177,9782,5226],{"class":230},[177,9784,1905],{"class":187},[177,9786,3734],{"class":183},[177,9788,9789],{"class":187}," (sCurrent[",[177,9791,493],{"class":230},[177,9793,9661],{"class":187},[177,9795,9381],{"class":194},[177,9797,1905],{"class":187},[177,9799,9800],{"class":183},"do\n",[177,9802,9803,9806,9808,9810,9812],{"class":179,"line":5474},[177,9804,9805],{"class":187},"                Delete(sCurrent, ",[177,9807,493],{"class":230},[177,9809,536],{"class":187},[177,9811,493],{"class":230},[177,9813,588],{"class":187},[177,9815,9816,9818,9820,9822,9824,9826,9829,9831,9833],{"class":179,"line":5480},[177,9817,9612],{"class":183},[177,9819,9780],{"class":187},[177,9821,5226],{"class":230},[177,9823,1905],{"class":187},[177,9825,3734],{"class":183},[177,9827,9828],{"class":187}," (sCurrent[Length(sCurrent)] = ",[177,9830,9381],{"class":194},[177,9832,1905],{"class":187},[177,9834,9800],{"class":183},[177,9836,9838,9841,9843],{"class":179,"line":9837},86,[177,9839,9840],{"class":187},"                Delete(sCurrent, Length(sCurrent), ",[177,9842,493],{"class":230},[177,9844,588],{"class":187},[177,9846,9848],{"class":179,"line":9847},87,[177,9849,218],{"emptyLinePlaceholder":30},[177,9851,9853,9855,9858,9860,9862,9864,9866],{"class":179,"line":9852},88,[177,9854,1878],{"class":183},[177,9856,9857],{"class":187}," RegWriteExpandStringValue(HKLM, ",[177,9859,9340],{"class":194},[177,9861,536],{"class":187},[177,9863,9345],{"class":194},[177,9865,9576],{"class":187},[177,9867,9351],{"class":183},[177,9869,9871],{"class":179,"line":9870},89,[177,9872,9625],{"class":183},[177,9874,9876],{"class":179,"line":9875},90,[177,9877,9878],{"class":187},"                RefreshEnvironment();\n",[177,9880,9882,9884],{"class":179,"line":9881},91,[177,9883,9731],{"class":183},[177,9885,198],{"class":187},[177,9887,9889,9891],{"class":179,"line":9888},92,[177,9890,9403],{"class":183},[177,9892,198],{"class":187},[177,9894,9896,9898],{"class":179,"line":9895},93,[177,9897,9410],{"class":183},[177,9899,198],{"class":187},[177,9901,9903,9905],{"class":179,"line":9902},94,[177,9904,9270],{"class":183},[177,9906,198],{"class":187},[17,9908,9909,9910,9917],{},"Finally, rather than using standard GitHub releases for distribution, I wanted to host the binaries myself to control the update flow. In the GitHub Actions workflow, I used ",[53,9911,9914],{"href":9912,"rel":9913},"https://rclone.org/",[57],[174,9915,9916],{},"rclone"," to automatically push the compiled artifacts directly to a Cloudflare R2 bucket, alongside a Bash script that parses the directory names to manage versioning:",[167,9919,9923],{"className":9920,"code":9921,"language":9922,"meta":15,"style":15},"language-yml shiki shiki-themes github-light github-dark github-dark","- name: Upload to R2\n        env:\n          CLOUDFLARE_R2_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}\n          # ...\n        run: |\n          VERSION_STR=\"${{ needs.build-linux.outputs.version }}\"\n          R2_PREFIX=\"releases/${VERSION_STR}\"\n\n          curl -fsSL https://rclone.org/install.sh | sudo bash\n          \n          # ... rclone configuration ...\n          \n          rclone copy build/ \"r2:${CLOUDFLARE_R2_BUCKET}/${R2_PREFIX}\" --progress --no-traverse\n","yml",[174,9924,9925,9939,9946,9956,9961,9971,9976,9981,9985,9990,9995,10000,10004],{"__ignoreMap":15},[177,9926,9927,9930,9934,9936],{"class":179,"line":180},[177,9928,9929],{"class":187},"- ",[177,9931,9933],{"class":9932},"sByVh","name",[177,9935,2976],{"class":187},[177,9937,9938],{"class":194},"Upload to R2\n",[177,9940,9941,9944],{"class":179,"line":25},[177,9942,9943],{"class":9932},"        env",[177,9945,6618],{"class":187},[177,9947,9948,9951,9953],{"class":179,"line":215},[177,9949,9950],{"class":9932},"          CLOUDFLARE_R2_ACCESS_KEY_ID",[177,9952,2976],{"class":187},[177,9954,9955],{"class":194},"${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}\n",[177,9957,9958],{"class":179,"line":221},[177,9959,9960],{"class":2925},"          # ...\n",[177,9962,9963,9966,9968],{"class":179,"line":250},[177,9964,9965],{"class":9932},"        run",[177,9967,2976],{"class":187},[177,9969,9970],{"class":183},"|\n",[177,9972,9973],{"class":179,"line":285},[177,9974,9975],{"class":194},"          VERSION_STR=\"${{ needs.build-linux.outputs.version }}\"\n",[177,9977,9978],{"class":179,"line":308},[177,9979,9980],{"class":194},"          R2_PREFIX=\"releases/${VERSION_STR}\"\n",[177,9982,9983],{"class":179,"line":329},[177,9984,218],{"emptyLinePlaceholder":30},[177,9986,9987],{"class":179,"line":350},[177,9988,9989],{"class":194},"          curl -fsSL https://rclone.org/install.sh | sudo bash\n",[177,9991,9992],{"class":179,"line":384},[177,9993,9994],{"class":194},"          \n",[177,9996,9997],{"class":179,"line":591},[177,9998,9999],{"class":194},"          # ... rclone configuration ...\n",[177,10001,10002],{"class":179,"line":613},[177,10003,9994],{"class":194},[177,10005,10006],{"class":179,"line":641},[177,10007,10008],{"class":194},"          rclone copy build/ \"r2:${CLOUDFLARE_R2_BUCKET}/${R2_PREFIX}\" --progress --no-traverse\n",[67,10010,10011],{"id":4591},"Finally",[17,10013,10014],{},"What started as a favor for a friend evolved into a full-scale application bridging the gap between low-level blockchain cryptography, desktop frameworks, and automated deployment pipelines. It was an absolute headache to piece together, but an incredibly awesome learning experience.",[17,10016,10017,10018,10022,10023,10027],{},"Give it a try, you can download it from its ",[53,10019,58],{"href":10020,"rel":10021},"https://solana.riavzon.com/download",[57],", and send me ",[53,10024,10026],{"href":10025},"/contact","feedback"," later.",[10029,10030,10031],"style",{},"html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sQeJH, html code.shiki .sQeJH{--shiki-light:#032F62;--shiki-default:#DBEDFF;--shiki-dark:#DBEDFF}html pre.shiki code .sAxt1, html code.shiki .sAxt1{--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-default:#85E89D;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sByVh, html code.shiki .sByVh{--shiki-light:#22863A;--shiki-default:#85E89D;--shiki-dark:#85E89D}",{"title":15,"searchDepth":25,"depth":25,"links":10033},[10034,10035,10039,10046],{"id":69,"depth":25,"text":70},{"id":149,"depth":25,"text":150,"children":10036},[10037,10038],{"id":406,"depth":215,"text":407},{"id":2259,"depth":215,"text":2260},{"id":5954,"depth":25,"text":5955,"children":10040},[10041,10042,10043,10044,10045],{"id":7242,"depth":215,"text":7243},{"id":8161,"depth":215,"text":8162},{"id":8183,"depth":215,"text":8184},{"id":8656,"depth":215,"text":8657},{"id":8994,"depth":215,"text":8995},{"id":4591,"depth":25,"text":10011},"26/06/2026","A deep dive into the technical realities of building Electron applications for the Solana Blockchain, from encryption, states to handling concurrent network operations.",{},"/articles/the-electron-experience","20",{"title":39,"description":10048},{"loc":10050},"articles/00.the-electron-experience",[10056,10057,10058],"Electron","Desktop","Crypto","xlDG0aDY99DwEn0lNef3qQ4L3ttmKWDb-6hZUCfW5Ug",{"id":10061,"title":10062,"body":10063,"date":11097,"description":11098,"extension":28,"head":27,"image":10072,"meta":11099,"navigation":30,"ogImage":27,"path":11101,"readingTime":11102,"robots":27,"schemaOrg":27,"seo":11103,"sitemap":11104,"stem":11105,"tags":11106,"__hash__":11110},"articles/articles/01.protecting-apis.md","Protecting Your API Endpoints with Bot Detector",{"type":7,"value":10064,"toc":11084},[10065,10068,10073,10082,10085,10088,10092,10101,10110,10114,10117,10120,10388,10392,10399,10403,10406,10415,10418,10490,10493,10673,10676,10680,10683,10693,10885,10889,10892,10895,10969,10972,10976,10979,11012,11015,11019,11033,11048,11051,11055,11062,11067,11071,11074,11081],[17,10066,10067],{},"APIs receive a wide range of automated traffic. Some of that traffic is useful and intentional. Other traffic probes for credentials, scrapes content at scale, or tries to find weaknesses.",[17,10069,10070],{},[45,10071],{"alt":47,"src":10072},"/articles/api.jpg",[17,10074,10075,10076,10081],{},"Protecting an endpoint requires combining multiple independent signals. A single rule rarely stops a determined attacker. ",[53,10077,10080],{"href":10078,"rel":10079},"https://docs.riavzon.com/docs/bot-detection",[57],"Bot Detector"," brings together reputation, fingerprinting, and behavioral analysis in a single pipeline.",[17,10083,10084],{},"Each signal has limits. IP reputation flags known bad ranges but attackers rotate addresses. User agent checks catch simple tools but not sophisticated ones. Timing and session signals reveal scripted behavior that other checks miss.",[17,10086,10087],{},"By combining signals you make evasion costly. An attacker must spoof multiple unrelated properties at once to avoid detection. That increases complexity and reduces the attack surface.",[67,10089,10091],{"id":10090},"how-it-fits","How it fits",[17,10093,10094,10095,10100],{},"Bot Detector is an Express middleware that runs a two phase pipeline of ",[53,10096,10099],{"href":10097,"rel":10098},"https://docs.riavzon.com/docs/bot-detection/checkers",[57],"checkers",". The cheap phase runs synchronous, in-memory checks first and stops early when the score crosses the ban threshold. The heavy phase runs async checks only when needed.",[17,10102,10103,10104,10109],{},"The detector loads binary databases compiled by ",[53,10105,10108],{"href":10106,"rel":10107},"https://docs.riavzon.com/docs/shield-base",[57],"Shield Base"," at startup. Those files provide fast IP, ASN, Tor, proxy, and user-agent pattern lookups. A canary cookie ties each browser or client session to a persistent server record used by behavioral checks.",[67,10111,10113],{"id":10112},"integrate-quickly-with-express","Integrate quickly with Express",[17,10115,10116],{},"Mount the middleware early in the request chain so it can short-circuit abusive requests before they reach business logic. The example below shows a minimal Express setup and a protected login route.",[17,10118,10119],{},"Initialize the detector once at startup, then mount the middleware and protect sensitive routes.",[167,10121,10124],{"className":169,"code":10122,"filename":10123,"language":172,"meta":15,"style":15},"import express from 'express';\nimport cookieParser from 'cookie-parser';\nimport { defineConfiguration, detectBots } from '@riavzon/bot-detector';\n\nconst app = express();\napp.use(cookieParser());\n\nawait defineConfiguration({\n  store: { main: { driver: 'sqlite', name: './bot-detector.db' } },\n  banScore: 100,\n  checkers: {\n    enableBehaviorRateCheck: { enable: true },\n    enableUaAndHeaderChecks: { enable: true }\n  }\n});\n\napp.use(detectBots());\n\napp.post('/auth/login', detectBots(), async (req, res) => {\n  if (req.botDetection?.banned) return res.status(403).json({ ok: false, reason: 'BOT_DETECTED' });\n  // continue with normal login flow\n  res.json({ ok: true });\n});\n","server.ts",[174,10125,10126,10140,10154,10168,10172,10186,10201,10205,10214,10231,10241,10246,10256,10265,10269,10273,10277,10290,10294,10329,10366,10371,10384],{"__ignoreMap":15},[177,10127,10128,10130,10133,10135,10138],{"class":179,"line":180},[177,10129,184],{"class":183},[177,10131,10132],{"class":187}," express ",[177,10134,191],{"class":183},[177,10136,10137],{"class":194}," 'express'",[177,10139,198],{"class":187},[177,10141,10142,10144,10147,10149,10152],{"class":179,"line":25},[177,10143,184],{"class":183},[177,10145,10146],{"class":187}," cookieParser ",[177,10148,191],{"class":183},[177,10150,10151],{"class":194}," 'cookie-parser'",[177,10153,198],{"class":187},[177,10155,10156,10158,10161,10163,10166],{"class":179,"line":215},[177,10157,184],{"class":183},[177,10159,10160],{"class":187}," { defineConfiguration, detectBots } ",[177,10162,191],{"class":183},[177,10164,10165],{"class":194}," '@riavzon/bot-detector'",[177,10167,198],{"class":187},[177,10169,10170],{"class":179,"line":221},[177,10171,218],{"emptyLinePlaceholder":30},[177,10173,10174,10176,10179,10181,10184],{"class":179,"line":250},[177,10175,445],{"class":183},[177,10177,10178],{"class":230}," app",[177,10180,234],{"class":183},[177,10182,10183],{"class":237}," express",[177,10185,717],{"class":187},[177,10187,10188,10191,10194,10196,10199],{"class":179,"line":285},[177,10189,10190],{"class":187},"app.",[177,10192,10193],{"class":237},"use",[177,10195,241],{"class":187},[177,10197,10198],{"class":237},"cookieParser",[177,10200,2144],{"class":187},[177,10202,10203],{"class":179,"line":308},[177,10204,218],{"emptyLinePlaceholder":30},[177,10206,10207,10209,10212],{"class":179,"line":329},[177,10208,2852],{"class":183},[177,10210,10211],{"class":237}," defineConfiguration",[177,10213,1478],{"class":187},[177,10215,10216,10219,10222,10225,10228],{"class":179,"line":350},[177,10217,10218],{"class":187},"  store: { main: { driver: ",[177,10220,10221],{"class":194},"'sqlite'",[177,10223,10224],{"class":187},", name: ",[177,10226,10227],{"class":194},"'./bot-detector.db'",[177,10229,10230],{"class":187}," } },\n",[177,10232,10233,10236,10239],{"class":179,"line":384},[177,10234,10235],{"class":187},"  banScore: ",[177,10237,10238],{"class":230},"100",[177,10240,464],{"class":187},[177,10242,10243],{"class":179,"line":591},[177,10244,10245],{"class":187},"  checkers: {\n",[177,10247,10248,10251,10253],{"class":179,"line":613},[177,10249,10250],{"class":187},"    enableBehaviorRateCheck: { enable: ",[177,10252,279],{"class":230},[177,10254,10255],{"class":187}," },\n",[177,10257,10258,10261,10263],{"class":179,"line":641},[177,10259,10260],{"class":187},"    enableUaAndHeaderChecks: { enable: ",[177,10262,279],{"class":230},[177,10264,496],{"class":187},[177,10266,10267],{"class":179,"line":647},[177,10268,790],{"class":187},[177,10270,10271],{"class":179,"line":670},[177,10272,387],{"class":187},[177,10274,10275],{"class":179,"line":701},[177,10276,218],{"emptyLinePlaceholder":30},[177,10278,10279,10281,10283,10285,10288],{"class":179,"line":720},[177,10280,10190],{"class":187},[177,10282,10193],{"class":237},[177,10284,241],{"class":187},[177,10286,10287],{"class":237},"detectBots",[177,10289,2144],{"class":187},[177,10291,10292],{"class":179,"line":725},[177,10293,218],{"emptyLinePlaceholder":30},[177,10295,10296,10298,10301,10303,10306,10308,10310,10312,10314,10316,10319,10321,10323,10325,10327],{"class":179,"line":738},[177,10297,10190],{"class":187},[177,10299,10300],{"class":237},"post",[177,10302,241],{"class":187},[177,10304,10305],{"class":194},"'/auth/login'",[177,10307,536],{"class":187},[177,10309,10287],{"class":237},[177,10311,4752],{"class":187},[177,10313,2861],{"class":183},[177,10315,523],{"class":187},[177,10317,10318],{"class":526},"req",[177,10320,536],{"class":187},[177,10322,2699],{"class":526},[177,10324,1905],{"class":187},[177,10326,1908],{"class":183},[177,10328,453],{"class":187},[177,10330,10331,10333,10336,10338,10341,10344,10346,10349,10351,10354,10357,10359,10361,10364],{"class":179,"line":750},[177,10332,3757],{"class":183},[177,10334,10335],{"class":187}," (req.botDetection?.banned) ",[177,10337,6251],{"class":183},[177,10339,10340],{"class":187}," res.",[177,10342,10343],{"class":237},"status",[177,10345,241],{"class":187},[177,10347,10348],{"class":230},"403",[177,10350,299],{"class":187},[177,10352,10353],{"class":237},"json",[177,10355,10356],{"class":187},"({ ok: ",[177,10358,3802],{"class":230},[177,10360,6259],{"class":187},[177,10362,10363],{"class":194},"'BOT_DETECTED'",[177,10365,5288],{"class":187},[177,10367,10368],{"class":179,"line":787},[177,10369,10370],{"class":2925},"  // continue with normal login flow\n",[177,10372,10373,10376,10378,10380,10382],{"class":179,"line":793},[177,10374,10375],{"class":187},"  res.",[177,10377,10353],{"class":237},[177,10379,10356],{"class":187},[177,10381,279],{"class":230},[177,10383,5288],{"class":187},[177,10385,10386],{"class":179,"line":798},[177,10387,387],{"class":187},[67,10389,10391],{"id":10390},"selective-enforcement","Selective enforcement",[17,10393,10394,10395,10398],{},"Protect only the endpoints that matter. Use ",[174,10396,10397],{},"detectBots()"," as a route-level middleware for high value paths. This keeps false positive surface small and preserves throughput for public endpoints.",[404,10400,10402],{"id":10401},"tuning-for-api-traffic","Tuning for API traffic",[17,10404,10405],{},"APIs often serve legitimate automated clients. That fact changes how you tune the detector. The two common approaches are explicit client identification and adaptive penalties.",[17,10407,10408,10409,10414],{},"Use API keys or client certificates to identify trusted automation. When a request carries a valid API key, mark it as a trusted client in the ",[53,10410,10413],{"href":10411,"rel":10412},"https://docs.riavzon.com/docs/bot-detection/guides/custom",[57],"custom context"," and either lower penalties or skip specific checks. When you cannot trust the client, apply the full pipeline.",[17,10416,10417],{},"Explanation: build a small custom context that flags API clients based on the presence of a known key.",[167,10419,10421],{"className":169,"code":10420,"filename":10123,"language":172,"meta":15,"style":15},"app.use(\n  detectBots\u003C{ isApiClient?: boolean }>((req) => ({\n    isApiClient: Boolean(req.get('x-api-key'))\n  }))\n);\n",[174,10422,10423,10431,10459,10481,10486],{"__ignoreMap":15},[177,10424,10425,10427,10429],{"class":179,"line":180},[177,10426,10190],{"class":187},[177,10428,10193],{"class":237},[177,10430,2886],{"class":187},[177,10432,10433,10436,10439,10442,10444,10447,10450,10452,10454,10456],{"class":179,"line":25},[177,10434,10435],{"class":237},"  detectBots",[177,10437,10438],{"class":187},"\u003C{ ",[177,10440,10441],{"class":526},"isApiClient",[177,10443,2529],{"class":183},[177,10445,10446],{"class":230}," boolean",[177,10448,10449],{"class":187}," }>((",[177,10451,10318],{"class":526},[177,10453,1905],{"class":187},[177,10455,1908],{"class":183},[177,10457,10458],{"class":187}," ({\n",[177,10460,10461,10464,10467,10470,10473,10475,10478],{"class":179,"line":215},[177,10462,10463],{"class":187},"    isApiClient: ",[177,10465,10466],{"class":237},"Boolean",[177,10468,10469],{"class":187},"(req.",[177,10471,10472],{"class":237},"get",[177,10474,241],{"class":187},[177,10476,10477],{"class":194},"'x-api-key'",[177,10479,10480],{"class":187},"))\n",[177,10482,10483],{"class":179,"line":221},[177,10484,10485],{"class":187},"  }))\n",[177,10487,10488],{"class":179,"line":250},[177,10489,588],{"class":187},[17,10491,10492],{},"Then implement a cheap-phase checker that treats flagged clients differently.",[167,10494,10497],{"className":169,"code":10495,"filename":10496,"language":172,"meta":15,"style":15},"import { CheckerRegistry } from '@riavzon/bot-detector';\nimport type { IBotChecker, ValidationContext } from '@riavzon/bot-detector';\n\nclass ApiClientChecker implements IBotChecker\u003C'API_CLIENT'> {\n  name = 'api-client-checker';\n  phase = 'cheap' as const;\n  isEnabled() { return true; }\n\n  run(ctx: ValidationContext) {\n    if (ctx.custom?.isApiClient) return { score: 0, reasons: [] };\n    return { score: 0, reasons: [] };\n  }\n}\n\nCheckerRegistry.register(new ApiClientChecker());\n","checkers/api-client-checker.ts",[174,10498,10499,10512,10528,10532,10554,10566,10582,10597,10601,10618,10635,10645,10649,10653,10657],{"__ignoreMap":15},[177,10500,10501,10503,10506,10508,10510],{"class":179,"line":180},[177,10502,184],{"class":183},[177,10504,10505],{"class":187}," { CheckerRegistry } ",[177,10507,191],{"class":183},[177,10509,10165],{"class":194},[177,10511,198],{"class":187},[177,10513,10514,10516,10519,10522,10524,10526],{"class":179,"line":25},[177,10515,184],{"class":183},[177,10517,10518],{"class":183}," type",[177,10520,10521],{"class":187}," { IBotChecker, ValidationContext } ",[177,10523,191],{"class":183},[177,10525,10165],{"class":194},[177,10527,198],{"class":187},[177,10529,10530],{"class":179,"line":215},[177,10531,218],{"emptyLinePlaceholder":30},[177,10533,10534,10537,10540,10543,10546,10548,10551],{"class":179,"line":221},[177,10535,10536],{"class":183},"class",[177,10538,10539],{"class":237}," ApiClientChecker",[177,10541,10542],{"class":183}," implements",[177,10544,10545],{"class":237}," IBotChecker",[177,10547,4143],{"class":187},[177,10549,10550],{"class":194},"'API_CLIENT'",[177,10552,10553],{"class":187},"> {\n",[177,10555,10556,10559,10561,10564],{"class":179,"line":250},[177,10557,10558],{"class":526},"  name",[177,10560,234],{"class":183},[177,10562,10563],{"class":194}," 'api-client-checker'",[177,10565,198],{"class":187},[177,10567,10568,10571,10573,10576,10578,10580],{"class":179,"line":285},[177,10569,10570],{"class":526},"  phase",[177,10572,234],{"class":183},[177,10574,10575],{"class":194}," 'cheap'",[177,10577,772],{"class":183},[177,10579,227],{"class":183},[177,10581,198],{"class":187},[177,10583,10584,10587,10590,10592,10594],{"class":179,"line":308},[177,10585,10586],{"class":237},"  isEnabled",[177,10588,10589],{"class":187},"() { ",[177,10591,6251],{"class":183},[177,10593,4551],{"class":230},[177,10595,10596],{"class":187},"; }\n",[177,10598,10599],{"class":179,"line":329},[177,10600,218],{"emptyLinePlaceholder":30},[177,10602,10603,10606,10608,10611,10613,10616],{"class":179,"line":350},[177,10604,10605],{"class":237},"  run",[177,10607,241],{"class":187},[177,10609,10610],{"class":526},"ctx",[177,10612,530],{"class":183},[177,10614,10615],{"class":237}," ValidationContext",[177,10617,1200],{"class":187},[177,10619,10620,10622,10625,10627,10630,10632],{"class":179,"line":384},[177,10621,932],{"class":183},[177,10623,10624],{"class":187}," (ctx.custom?.isApiClient) ",[177,10626,6251],{"class":183},[177,10628,10629],{"class":187}," { score: ",[177,10631,5226],{"class":230},[177,10633,10634],{"class":187},", reasons: [] };\n",[177,10636,10637,10639,10641,10643],{"class":179,"line":591},[177,10638,728],{"class":183},[177,10640,10629],{"class":187},[177,10642,5226],{"class":230},[177,10644,10634],{"class":187},[177,10646,10647],{"class":179,"line":613},[177,10648,790],{"class":187},[177,10650,10651],{"class":179,"line":641},[177,10652,1536],{"class":187},[177,10654,10655],{"class":179,"line":647},[177,10656,218],{"emptyLinePlaceholder":30},[177,10658,10659,10662,10665,10667,10669,10671],{"class":179,"line":670},[177,10660,10661],{"class":187},"CheckerRegistry.",[177,10663,10664],{"class":237},"register",[177,10666,241],{"class":187},[177,10668,8038],{"class":183},[177,10670,10539],{"class":237},[177,10672,2144],{"class":187},[17,10674,10675],{},"This pattern keeps the pipeline flexible. Trusted clients retain their throughput while unknown clients face the full set of checks.",[67,10677,10679],{"id":10678},"custom-checkers-for-business-signals","Custom checkers for business signals",[17,10681,10682],{},"Custom checkers let you encode domain knowledge into the same scoring pipeline. Examples include plan tier limits, internal IP bypasses, or stricter rules for account creation endpoints.",[17,10684,10685,10686,10692],{},"Explanation: a checker inspects ",[53,10687,10689],{"href":10411,"rel":10688},[57],[174,10690,10691],{},"ctx.custom"," and returns a score and reason codes. Register it at startup and the pipeline picks it up automatically.",[167,10694,10697],{"className":169,"code":10695,"filename":10696,"language":172,"meta":15,"style":15},"import { CheckerRegistry } from '@riavzon/bot-detector';\nimport type { IBotChecker, ValidationContext } from '@riavzon/bot-detector';\n\nclass PlanAbuseChecker implements IBotChecker\u003C'PLAN_ABUSE'> {\n  name = 'plan-abuse';\n  phase = 'cheap' as const;\n  isEnabled() { return true; }\n\n  run(ctx: ValidationContext\u003C{ plan?: string }>) {\n    if (ctx.custom?.plan === 'free' && ctx.geoData?.proxy) {\n      return { score: 20, reasons: ['PLAN_ABUSE'] };\n    }\n    return { score: 0, reasons: [] };\n  }\n}\n\nCheckerRegistry.register(new PlanAbuseChecker());\n","checkers/plan-abuse-checker.ts",[174,10698,10699,10711,10725,10729,10747,10758,10772,10784,10788,10812,10829,10845,10849,10859,10863,10867,10871],{"__ignoreMap":15},[177,10700,10701,10703,10705,10707,10709],{"class":179,"line":180},[177,10702,184],{"class":183},[177,10704,10505],{"class":187},[177,10706,191],{"class":183},[177,10708,10165],{"class":194},[177,10710,198],{"class":187},[177,10712,10713,10715,10717,10719,10721,10723],{"class":179,"line":25},[177,10714,184],{"class":183},[177,10716,10518],{"class":183},[177,10718,10521],{"class":187},[177,10720,191],{"class":183},[177,10722,10165],{"class":194},[177,10724,198],{"class":187},[177,10726,10727],{"class":179,"line":215},[177,10728,218],{"emptyLinePlaceholder":30},[177,10730,10731,10733,10736,10738,10740,10742,10745],{"class":179,"line":221},[177,10732,10536],{"class":183},[177,10734,10735],{"class":237}," PlanAbuseChecker",[177,10737,10542],{"class":183},[177,10739,10545],{"class":237},[177,10741,4143],{"class":187},[177,10743,10744],{"class":194},"'PLAN_ABUSE'",[177,10746,10553],{"class":187},[177,10748,10749,10751,10753,10756],{"class":179,"line":250},[177,10750,10558],{"class":526},[177,10752,234],{"class":183},[177,10754,10755],{"class":194}," 'plan-abuse'",[177,10757,198],{"class":187},[177,10759,10760,10762,10764,10766,10768,10770],{"class":179,"line":285},[177,10761,10570],{"class":526},[177,10763,234],{"class":183},[177,10765,10575],{"class":194},[177,10767,772],{"class":183},[177,10769,227],{"class":183},[177,10771,198],{"class":187},[177,10773,10774,10776,10778,10780,10782],{"class":179,"line":308},[177,10775,10586],{"class":237},[177,10777,10589],{"class":187},[177,10779,6251],{"class":183},[177,10781,4551],{"class":230},[177,10783,10596],{"class":187},[177,10785,10786],{"class":179,"line":329},[177,10787,218],{"emptyLinePlaceholder":30},[177,10789,10790,10792,10794,10796,10798,10800,10802,10805,10807,10809],{"class":179,"line":350},[177,10791,10605],{"class":237},[177,10793,241],{"class":187},[177,10795,10610],{"class":526},[177,10797,530],{"class":183},[177,10799,10615],{"class":237},[177,10801,10438],{"class":187},[177,10803,10804],{"class":526},"plan",[177,10806,2529],{"class":183},[177,10808,544],{"class":230},[177,10810,10811],{"class":187}," }>) {\n",[177,10813,10814,10816,10819,10821,10824,10826],{"class":179,"line":384},[177,10815,932],{"class":183},[177,10817,10818],{"class":187}," (ctx.custom?.plan ",[177,10820,1268],{"class":183},[177,10822,10823],{"class":194}," 'free'",[177,10825,2572],{"class":183},[177,10827,10828],{"class":187}," ctx.geoData?.proxy) {\n",[177,10830,10831,10833,10835,10837,10840,10842],{"class":179,"line":591},[177,10832,2712],{"class":183},[177,10834,10629],{"class":187},[177,10836,10051],{"class":230},[177,10838,10839],{"class":187},", reasons: [",[177,10841,10744],{"class":194},[177,10843,10844],{"class":187},"] };\n",[177,10846,10847],{"class":179,"line":613},[177,10848,967],{"class":187},[177,10850,10851,10853,10855,10857],{"class":179,"line":641},[177,10852,728],{"class":183},[177,10854,10629],{"class":187},[177,10856,5226],{"class":230},[177,10858,10634],{"class":187},[177,10860,10861],{"class":179,"line":647},[177,10862,790],{"class":187},[177,10864,10865],{"class":179,"line":670},[177,10866,1536],{"class":187},[177,10868,10869],{"class":179,"line":701},[177,10870,218],{"emptyLinePlaceholder":30},[177,10872,10873,10875,10877,10879,10881,10883],{"class":179,"line":720},[177,10874,10661],{"class":187},[177,10876,10664],{"class":237},[177,10878,241],{"class":187},[177,10880,8038],{"class":183},[177,10882,10735],{"class":237},[177,10884,2144],{"class":187},[67,10886,10888],{"id":10887},"testing-and-validation","Testing and validation",[17,10890,10891],{},"Test in three layers: unit test checkers, integration test the middleware, and run traffic simulations that mirror expected attacks. Start conservative and raise penalties after you observe real traffic.",[17,10893,10894],{},"a simple script can generate repeated login attempts to validate that cheap-phase short-circuits obvious attacks.",[167,10896,10899],{"className":5964,"code":10897,"filename":10898,"language":5966,"meta":15,"style":15},"# 50 quick login attempts\nfor i in {1..50}; do\n  curl -s -X POST https://api.example.com/auth/login \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"username\":\"test\",\"password\":\"wrong\"}'\ndone\n","Terminal",[174,10900,10901,10906,10926,10946,10956,10964],{"__ignoreMap":15},[177,10902,10903],{"class":179,"line":180},[177,10904,10905],{"class":2925},"# 50 quick login attempts\n",[177,10907,10908,10911,10913,10916,10919,10922,10924],{"class":179,"line":25},[177,10909,10910],{"class":183},"for",[177,10912,4278],{"class":187},[177,10914,10915],{"class":183},"in",[177,10917,10918],{"class":187}," {",[177,10920,10921],{"class":237},"1..50}",[177,10923,7491],{"class":187},[177,10925,9800],{"class":183},[177,10927,10928,10931,10934,10937,10940,10943],{"class":179,"line":215},[177,10929,10930],{"class":237},"  curl",[177,10932,10933],{"class":230}," -s",[177,10935,10936],{"class":230}," -X",[177,10938,10939],{"class":194}," POST",[177,10941,10942],{"class":194}," https://api.example.com/auth/login",[177,10944,10945],{"class":230}," \\\n",[177,10947,10948,10951,10954],{"class":179,"line":221},[177,10949,10950],{"class":230},"    -H",[177,10952,10953],{"class":194}," \"Content-Type: application/json\"",[177,10955,10945],{"class":230},[177,10957,10958,10961],{"class":179,"line":250},[177,10959,10960],{"class":230},"    -d",[177,10962,10963],{"class":194}," '{\"username\":\"test\",\"password\":\"wrong\"}'\n",[177,10965,10966],{"class":179,"line":285},[177,10967,10968],{"class":183},"done\n",[17,10970,10971],{},"Observe the detector logs and metrics during the run. Confirm that banned requests return 403 and that the application handlers never execute for those requests.",[67,10973,10975],{"id":10974},"observability-and-metrics","Observability and metrics",[17,10977,10978],{},"Track these signals per endpoint and per reason code. They guide safe tuning and reveal blind spots.",[412,10980,10981,10988,10994,11000],{},[415,10982,10983,10987],{},[10984,10985,10986],"strong",{},"Ban rate",": count of banned requests by endpoint.",[415,10989,10990,10993],{},[10984,10991,10992],{},"Decision score distribution",": histogram of scores at decision time.",[415,10995,10996,10999],{},[10984,10997,10998],{},"Cheap vs heavy phase time",": time spent in each phase.",[415,11001,11002,11005,11006,1543,11009,65],{},[10984,11003,11004],{},"Generation counts",": entries written to ",[174,11007,11008],{},"banned.mmdb",[174,11010,11011],{},"highRisk.mmdb",[17,11013,11014],{},"Use your existing monitoring stack to alert on sudden increases in banned counts or generation volume. Those are reliable indicators of an active campaign.",[67,11016,11018],{"id":11017},"operations-and-generation","Operations and generation",[17,11020,11021,11022,11027,11028,1543,11030,11032],{},"Periodically run ",[53,11023,11026],{"href":11024,"rel":11025},"https://docs.riavzon.com/docs/bot-detection/guides/generate",[57],"generation"," to compile ",[174,11029,11008],{},[174,11031,11011],{}," from recent history. The detector hot reloads updated files automatically.",[167,11034,11036],{"className":5964,"code":11035,"filename":10898,"language":5966,"meta":15,"style":15},"npx @riavzon/bot-detector generate\n",[174,11037,11038],{"__ignoreMap":15},[177,11039,11040,11042,11045],{"class":179,"line":180},[177,11041,5973],{"class":237},[177,11043,11044],{"class":194}," @riavzon/bot-detector",[177,11046,11047],{"class":194}," generate\n",[17,11049,11050],{},"Start with nightly generation for moderate traffic. Move to hourly generation only if you see frequent repeat offenders that need immediate blocking.",[67,11052,11054],{"id":11053},"progressive-enforcement-policy","Progressive enforcement policy",[17,11056,11057,11058,11061],{},"Roll out enforcement in three steps. Detect, observe, then apply mitigation. Begin by logging decisions without blocking. Next, add ",[174,11059,11060],{},"highRisk"," compilations to bias the cheap phase. Finally, enforce bans for repeat offenders.",[3180,11063,11064],{},[17,11065,11066],{},"Start conservative when you deploy detection to avoid impacting legitimate users. Use metrics to move from observation to enforcement.",[67,11068,11070],{"id":11069},"summary","Summary",[17,11072,11073],{},"Protecting API endpoints requires signals that are hard to spoof together. Bot Detector combines compiled reputation data, fingerprint checks, and behavioral analytics into a two phase pipeline. Use API keys and custom checkers to handle legitimate automation. Tune by observing live traffic and increase enforcement in measured steps.",[17,11075,11076,11077,65],{},"Learn more about the module in its ",[53,11078,11080],{"href":10078,"rel":11079},[57],"docs",[10029,11082,11083],{},"html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":15,"searchDepth":25,"depth":25,"links":11085},[11086,11087,11088,11091,11092,11093,11094,11095,11096],{"id":10090,"depth":25,"text":10091},{"id":10112,"depth":25,"text":10113},{"id":10390,"depth":25,"text":10391,"children":11089},[11090],{"id":10401,"depth":215,"text":10402},{"id":10678,"depth":25,"text":10679},{"id":10887,"depth":25,"text":10888},{"id":10974,"depth":25,"text":10975},{"id":11017,"depth":25,"text":11018},{"id":11053,"depth":25,"text":11054},{"id":11069,"depth":25,"text":11070},"2026-04-16","A practical, in-depth guide to protecting API endpoints using Bot Detector's layered pipeline, session binding, and custom checkers.",{"author":11100},"Sergio","/articles/protecting-apis","20 min read",{"title":10062,"description":11098},{"loc":11101},"articles/01.protecting-apis",[11107,11108,11109],"Security","Bot Detection","API","pf1fqyJkp_E3y68ITm5CHpsmsvylqgF1Q1jl4398njg",{"id":11112,"title":11113,"body":11114,"date":12084,"description":11123,"extension":28,"head":27,"image":11120,"meta":12085,"navigation":30,"ogImage":27,"path":12086,"readingTime":12087,"robots":27,"schemaOrg":27,"seo":12088,"sitemap":12089,"stem":12090,"tags":12091,"__hash__":12096},"articles/articles/02.5-amazing-raycast-nuxt-snippets.md","5 Amazing Raycast Snippets for Enhancing Your Nuxt (Vue) Projects",{"type":7,"value":11115,"toc":12068},[11116,11121,11124,11128,11131,11135,11138,11148,11154,11161,11166,11175,11353,11360,11369,11377,11455,11462,11468,11476,11666,11673,11679,11686,11749,11756,11761,11769,12029,12033,12038,12041,12045,12048,12052,12055,12059,12062,12065],[17,11117,11118],{},[45,11119],{"alt":47,"src":11120},"/articles/5-raycast-snippets.jpg",[17,11122,11123],{},"In the realm of web development, where efficiency is as valuable as expertise, tools that streamline and simplify our workflow are indispensable. Among these, Raycast snippets emerge as a powerful ally, especially for those working with Nuxt and Vue frameworks. But what exactly are these snippets, and how can they transform your development experience?",[67,11125,11127],{"id":11126},"what-are-raycast-snippets","What Are Raycast Snippets?",[17,11129,11130],{},"Raycast snippets are small, reusable pieces of text or code that can be quickly inserted into your work. Think of them as shortcuts for frequently used content - whether it's code, canned email responses, or even emojis. They are designed to save time and reduce repetitive typing, allowing developers and writers to work more efficiently.",[67,11132,11134],{"id":11133},"how-to-use-raycast-snippets","How to Use Raycast Snippets",[17,11136,11137],{},"Using Raycast snippets is straightforward. Once you've created or imported a snippet within the Raycast app, you can assign a specific keyword to it. This keyword acts as a trigger - whenever you type it in any application, the snippet will automatically expand in place, replacing the keyword with the full text or code of the snippet.",[17,11139,11140,11141,11144,11145,11147],{},"For instance, if you have a snippet for a standard email sign-off, you can assign a keyword like ",[174,11142,11143],{},"sig1",". Typing ",[174,11146,11143],{}," in an email will then automatically expand to the full signature text. This feature is especially useful in coding, where you can have snippets for common code patterns or configurations.",[17,11149,11150],{},[45,11151],{"alt":11152,"src":11153},"snippets-exemple","/articles/snippets-exemple.gif",[404,11155,11157,11158],{"id":11156},"component-template-comp","Component Template: ",[174,11159,11160],{},"!comp",[17,11162,2432,11163,11165],{},[174,11164,11160],{}," snippet is a basic yet powerful template for creating new Vue components. It includes a script setup with TypeScript support, a template section, and scoped styling. This snippet is ideal for rapidly scaffolding new components in your project.",[17,11167,11168,11171,11172,11174],{},[10984,11169,11170],{},"Usage Example:"," Use ",[174,11173,11160],{}," to quickly create new Vue components, ensuring consistency and saving time on setup.",[167,11176,11181],{"className":11177,"code":11178,"filename":11179,"language":11180,"meta":15,"style":15},"language-vue shiki shiki-themes github-light github-dark github-dark","\u003Cscript setup lang=\"ts\">\nimport type { PropType } from \"vue\";\n\nconst props = defineProps({\n item: {\n  type: String, \n  required: true\n }\n});\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>{{ item }}\u003C/h1>\n  \u003C/div>\n\u003C/template>\n\n\u003Cstyle scoped>\n\n\u003C/style>\n","MyComponent.vue","vue",[174,11182,11183,11204,11220,11224,11238,11243,11248,11255,11259,11263,11272,11276,11284,11294,11309,11318,11326,11330,11341,11345],{"__ignoreMap":15},[177,11184,11185,11187,11190,11193,11196,11198,11201],{"class":179,"line":180},[177,11186,4143],{"class":187},[177,11188,11189],{"class":9932},"script",[177,11191,11192],{"class":237}," setup",[177,11194,11195],{"class":237}," lang",[177,11197,984],{"class":187},[177,11199,11200],{"class":194},"\"ts\"",[177,11202,11203],{"class":187},">\n",[177,11205,11206,11208,11210,11213,11215,11218],{"class":179,"line":25},[177,11207,184],{"class":183},[177,11209,10518],{"class":183},[177,11211,11212],{"class":187}," { PropType } ",[177,11214,191],{"class":183},[177,11216,11217],{"class":194}," \"vue\"",[177,11219,198],{"class":187},[177,11221,11222],{"class":179,"line":215},[177,11223,218],{"emptyLinePlaceholder":30},[177,11225,11226,11228,11231,11233,11236],{"class":179,"line":221},[177,11227,445],{"class":183},[177,11229,11230],{"class":230}," props",[177,11232,234],{"class":183},[177,11234,11235],{"class":237}," defineProps",[177,11237,1478],{"class":187},[177,11239,11240],{"class":179,"line":250},[177,11241,11242],{"class":187}," item: {\n",[177,11244,11245],{"class":179,"line":285},[177,11246,11247],{"class":187},"  type: String, \n",[177,11249,11250,11253],{"class":179,"line":308},[177,11251,11252],{"class":187},"  required: ",[177,11254,2116],{"class":230},[177,11256,11257],{"class":179,"line":329},[177,11258,496],{"class":187},[177,11260,11261],{"class":179,"line":350},[177,11262,387],{"class":187},[177,11264,11265,11268,11270],{"class":179,"line":384},[177,11266,11267],{"class":187},"\u003C/",[177,11269,11189],{"class":9932},[177,11271,11203],{"class":187},[177,11273,11274],{"class":179,"line":591},[177,11275,218],{"emptyLinePlaceholder":30},[177,11277,11278,11280,11282],{"class":179,"line":613},[177,11279,4143],{"class":187},[177,11281,13],{"class":9932},[177,11283,11203],{"class":187},[177,11285,11286,11289,11292],{"class":179,"line":641},[177,11287,11288],{"class":187},"  \u003C",[177,11290,11291],{"class":9932},"div",[177,11293,11203],{"class":187},[177,11295,11296,11299,11302,11305,11307],{"class":179,"line":647},[177,11297,11298],{"class":187},"    \u003C",[177,11300,11301],{"class":9932},"h1",[177,11303,11304],{"class":187},">{{ item }}\u003C/",[177,11306,11301],{"class":9932},[177,11308,11203],{"class":187},[177,11310,11311,11314,11316],{"class":179,"line":670},[177,11312,11313],{"class":187},"  \u003C/",[177,11315,11291],{"class":9932},[177,11317,11203],{"class":187},[177,11319,11320,11322,11324],{"class":179,"line":701},[177,11321,11267],{"class":187},[177,11323,13],{"class":9932},[177,11325,11203],{"class":187},[177,11327,11328],{"class":179,"line":720},[177,11329,218],{"emptyLinePlaceholder":30},[177,11331,11332,11334,11336,11339],{"class":179,"line":725},[177,11333,4143],{"class":187},[177,11335,10029],{"class":9932},[177,11337,11338],{"class":237}," scoped",[177,11340,11203],{"class":187},[177,11342,11343],{"class":179,"line":738},[177,11344,218],{"emptyLinePlaceholder":30},[177,11346,11347,11349,11351],{"class":179,"line":750},[177,11348,11267],{"class":187},[177,11350,10029],{"class":9932},[177,11352,11203],{"class":187},[404,11354,11356,11357],{"id":11355},"api-handler-template-api","API Handler Template: ",[174,11358,11359],{},"!api",[17,11361,11362,11363,11365,11366,11368],{},"Handling API requests is a common task in modern web applications. The ",[174,11364,11359],{}," snippet provides a template for creating API handlers using ",[174,11367,404],{},", a lightweight HTTP toolkit. This snippet streamlines the process of setting up API routes and handling requests.",[17,11370,11371,11373,11374,11376],{},[10984,11372,11170],{}," Implement the ",[174,11375,11359],{}," snippet for creating efficient API routes in your Nuxt application, especially when dealing with CRUD operations.",[167,11378,11381],{"className":169,"code":11379,"filename":11380,"language":172,"meta":15,"style":15},"import { H3Event } from \"h3\";\n\nexport default defineEventHandler(async (event: H3Event) => {\n  const body = await readBody(event);\n  // your_api_logic\n});\n","~/server/api/MyHandler.ts",[174,11382,11383,11397,11401,11429,11446,11451],{"__ignoreMap":15},[177,11384,11385,11387,11390,11392,11395],{"class":179,"line":180},[177,11386,184],{"class":183},[177,11388,11389],{"class":187}," { H3Event } ",[177,11391,191],{"class":183},[177,11393,11394],{"class":194}," \"h3\"",[177,11396,198],{"class":187},[177,11398,11399],{"class":179,"line":25},[177,11400,218],{"emptyLinePlaceholder":30},[177,11402,11403,11405,11407,11410,11412,11414,11416,11418,11420,11423,11425,11427],{"class":179,"line":215},[177,11404,224],{"class":183},[177,11406,8260],{"class":183},[177,11408,11409],{"class":237}," defineEventHandler",[177,11411,241],{"class":187},[177,11413,2861],{"class":183},[177,11415,523],{"class":187},[177,11417,7630],{"class":526},[177,11419,530],{"class":183},[177,11421,11422],{"class":237}," H3Event",[177,11424,1905],{"class":187},[177,11426,1908],{"class":183},[177,11428,453],{"class":187},[177,11430,11431,11433,11436,11438,11440,11443],{"class":179,"line":221},[177,11432,3671],{"class":183},[177,11434,11435],{"class":230}," body",[177,11437,234],{"class":183},[177,11439,1600],{"class":183},[177,11441,11442],{"class":237}," readBody",[177,11444,11445],{"class":187},"(event);\n",[177,11447,11448],{"class":179,"line":250},[177,11449,11450],{"class":2925},"  // your_api_logic\n",[177,11452,11453],{"class":179,"line":285},[177,11454,387],{"class":187},[404,11456,11458,11459],{"id":11457},"state-management-with-pinia-store","State Management with Pinia: ",[174,11460,11461],{},"!store",[17,11463,11464,11465,11467],{},"State management is crucial in large-scale applications. The ",[174,11466,11461],{}," snippet utilizes Pinia, a Vue store, offering a structured template for managing application state. It includes a state definition, getters, and actions.",[17,11469,11470,11472,11473,11475],{},[10984,11471,11170],{}," Utilize ",[174,11474,11461],{}," for setting up store modules in your Nuxt/Vue app, managing state more effectively and cleanly. the { clipboard } while be replaced by your actual clipboard.",[167,11477,11483],{"className":169,"code":11478,"filename":11479,"highlights":11480,"language":172,"meta":11482,"style":15},"import { defineStore } from 'pinia';\n\ntype {clipboard}Store = { \n  count: number;\n} \n\nexport const use{clipboard}Store = defineStore('{clipboard}', {\n  state: (): {clipboard}Store => ({ \n    count: 0,\n  }), \n  getters: { \n    getCount(): number { \n      return this.count; \n    }\n  },\n  actions: { \n    increment() {\n      this.count++; \n    }, \n  } \n});\n","~/store/{clipboard}.ts",[11481],0,"[~/store/.ts]",[174,11484,11485,11499,11503,11512,11517,11522,11526,11550,11576,11585,11590,11595,11609,11619,11623,11627,11632,11640,11652,11657,11662],{"__ignoreMap":15},[177,11486,11487,11489,11492,11494,11497],{"class":179,"line":180},[177,11488,184],{"class":183},[177,11490,11491],{"class":187}," { defineStore } ",[177,11493,191],{"class":183},[177,11495,11496],{"class":194}," 'pinia'",[177,11498,198],{"class":187},[177,11500,11501],{"class":179,"line":25},[177,11502,218],{"emptyLinePlaceholder":30},[177,11504,11505,11508,11510],{"class":179,"line":215},[177,11506,11507],{"class":187},"type {clipboard}Store ",[177,11509,984],{"class":183},[177,11511,3794],{"class":187},[177,11513,11514],{"class":179,"line":221},[177,11515,11516],{"class":187},"  count: number;\n",[177,11518,11519],{"class":179,"line":250},[177,11520,11521],{"class":187},"} \n",[177,11523,11524],{"class":179,"line":285},[177,11525,218],{"emptyLinePlaceholder":30},[177,11527,11528,11530,11532,11535,11538,11540,11543,11545,11548],{"class":179,"line":308},[177,11529,224],{"class":183},[177,11531,227],{"class":183},[177,11533,11534],{"class":230}," use",[177,11536,11537],{"class":187},"{clipboard}Store ",[177,11539,984],{"class":183},[177,11541,11542],{"class":237}," defineStore",[177,11544,241],{"class":187},[177,11546,11547],{"class":194},"'{clipboard}'",[177,11549,247],{"class":187},[177,11551,11552,11555,11558,11560,11562,11565,11568,11571,11573],{"class":179,"line":329},[177,11553,11554],{"class":237},"  state",[177,11556,11557],{"class":187},": ()",[177,11559,530],{"class":183},[177,11561,10918],{"class":187},[177,11563,11564],{"class":526},"clipboard",[177,11566,11567],{"class":187},"}",[177,11569,11570],{"class":237},"Store",[177,11572,555],{"class":183},[177,11574,11575],{"class":187}," ({ \n",[177,11577,11578,11581,11583],{"class":179,"line":350},[177,11579,11580],{"class":187},"    count: ",[177,11582,5226],{"class":230},[177,11584,464],{"class":187},[177,11586,11587],{"class":179,"line":384},[177,11588,11589],{"class":187},"  }), \n",[177,11591,11592],{"class":179,"line":591},[177,11593,11594],{"class":187},"  getters: { \n",[177,11596,11597,11600,11603,11605,11607],{"class":179,"line":613},[177,11598,11599],{"class":237},"    getCount",[177,11601,11602],{"class":187},"()",[177,11604,530],{"class":183},[177,11606,1863],{"class":230},[177,11608,3794],{"class":187},[177,11610,11611,11613,11616],{"class":179,"line":641},[177,11612,2712],{"class":183},[177,11614,11615],{"class":230}," this",[177,11617,11618],{"class":187},".count; \n",[177,11620,11621],{"class":179,"line":647},[177,11622,967],{"class":187},[177,11624,11625],{"class":179,"line":670},[177,11626,4864],{"class":187},[177,11628,11629],{"class":179,"line":701},[177,11630,11631],{"class":187},"  actions: { \n",[177,11633,11634,11637],{"class":179,"line":720},[177,11635,11636],{"class":237},"    increment",[177,11638,11639],{"class":187},"() {\n",[177,11641,11642,11645,11648,11650],{"class":179,"line":725},[177,11643,11644],{"class":230},"      this",[177,11646,11647],{"class":187},".count",[177,11649,4293],{"class":183},[177,11651,882],{"class":187},[177,11653,11654],{"class":179,"line":738},[177,11655,11656],{"class":187},"    }, \n",[177,11658,11659],{"class":179,"line":750},[177,11660,11661],{"class":187},"  } \n",[177,11663,11664],{"class":179,"line":787},[177,11665,387],{"class":187},[404,11667,11669,11670],{"id":11668},"composable-function-template-cps","Composable Function Template: ",[174,11671,11672],{},"!cps",[17,11674,11675,11676,11678],{},"Composable functions in Vue 3 bring reusability and organization to your code. The ",[174,11677,11672],{}," snippet offers a template for creating these functions, aiding in maintaining a clean and modular codebase.",[17,11680,11681,11171,11683,11685],{},[10984,11682,11170],{},[174,11684,11672],{}," for creating reusable composable functions that can be shared across components, enhancing code reusability and maintainability.",[167,11687,11690],{"className":169,"code":11688,"filename":11689,"language":172,"meta":15,"style":15},"export function use{clipboard}() {\n  const {clipboard} = ref(null);\n  \n  // Composable logic\n  \n  return { {clipboard} };\n}\n","~/composables/useComposables.ts",[174,11691,11692,11703,11725,11729,11734,11738,11745],{"__ignoreMap":15},[177,11693,11694,11696,11698,11700],{"class":179,"line":180},[177,11695,224],{"class":183},[177,11697,1410],{"class":183},[177,11699,11534],{"class":237},[177,11701,11702],{"class":187},"{clipboard}() {\n",[177,11704,11705,11707,11709,11711,11713,11715,11718,11720,11723],{"class":179,"line":25},[177,11706,3671],{"class":183},[177,11708,10918],{"class":187},[177,11710,11564],{"class":230},[177,11712,8778],{"class":187},[177,11714,984],{"class":183},[177,11716,11717],{"class":237}," ref",[177,11719,241],{"class":187},[177,11721,11722],{"class":230},"null",[177,11724,588],{"class":187},[177,11726,11727],{"class":179,"line":215},[177,11728,5477],{"class":187},[177,11730,11731],{"class":179,"line":221},[177,11732,11733],{"class":2925},"  // Composable logic\n",[177,11735,11736],{"class":179,"line":250},[177,11737,5477],{"class":187},[177,11739,11740,11742],{"class":179,"line":285},[177,11741,7365],{"class":183},[177,11743,11744],{"class":187}," { {clipboard} };\n",[177,11746,11747],{"class":179,"line":308},[177,11748,1536],{"class":187},[404,11750,11752,11753],{"id":11751},"fetching-data-with-composition-api-fcomp","Fetching Data with Composition API: ",[174,11754,11755],{},"!fcomp",[17,11757,2432,11758,11760],{},[174,11759,11755],{}," snippet is designed for fetching data using Vue's Composition API. It provides a setup for making HTTP requests, handling loading states, and managing errors, all within a component.",[17,11762,11763,11765,11766,11768],{},[10984,11764,11170],{}," Implement ",[174,11767,11755],{}," in scenarios where you need to fetch data from an API, providing a robust structure for data fetching and state management.",[167,11770,11772],{"className":11177,"code":11771,"filename":11179,"language":11180,"meta":15,"style":15},"\u003Cscript setup lang=\"ts\">\nconst { data, pending, error, refresh } = useFetch(\"your_url\", { \n  immediate: false,\n  watch: false,\n});\n\nfunction loadData() {\n  await refresh();\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cbutton @click=\"loadData\">Load Data\u003C/button>\n    \u003Cdiv v-if=\"pending\">Loading...\u003C/div>\n    \u003Cdiv v-if=\"error\">{{ error }}\u003C/div>\n    \u003Cdiv v-if=\"data\">{{ data }}\u003C/div>\n  \u003C/div>\n\u003C/template>\n\n\u003Cstyle scoped>\n/* composant styles */\n\u003C/style>\n",[174,11773,11774,11790,11827,11836,11845,11849,11853,11862,11871,11875,11883,11887,11895,11903,11925,11946,11966,11986,11994,12002,12006,12016,12021],{"__ignoreMap":15},[177,11775,11776,11778,11780,11782,11784,11786,11788],{"class":179,"line":180},[177,11777,4143],{"class":187},[177,11779,11189],{"class":9932},[177,11781,11192],{"class":237},[177,11783,11195],{"class":237},[177,11785,984],{"class":187},[177,11787,11200],{"class":194},[177,11789,11203],{"class":187},[177,11791,11792,11794,11796,11799,11801,11804,11806,11808,11810,11813,11815,11817,11820,11822,11825],{"class":179,"line":25},[177,11793,445],{"class":183},[177,11795,7643],{"class":187},[177,11797,11798],{"class":230},"data",[177,11800,536],{"class":187},[177,11802,11803],{"class":230},"pending",[177,11805,536],{"class":187},[177,11807,1192],{"class":230},[177,11809,536],{"class":187},[177,11811,11812],{"class":230},"refresh",[177,11814,7661],{"class":187},[177,11816,984],{"class":183},[177,11818,11819],{"class":237}," useFetch",[177,11821,241],{"class":187},[177,11823,11824],{"class":194},"\"your_url\"",[177,11826,2963],{"class":187},[177,11828,11829,11832,11834],{"class":179,"line":215},[177,11830,11831],{"class":187},"  immediate: ",[177,11833,3802],{"class":230},[177,11835,464],{"class":187},[177,11837,11838,11841,11843],{"class":179,"line":221},[177,11839,11840],{"class":187},"  watch: ",[177,11842,3802],{"class":230},[177,11844,464],{"class":187},[177,11846,11847],{"class":179,"line":250},[177,11848,387],{"class":187},[177,11850,11851],{"class":179,"line":285},[177,11852,218],{"emptyLinePlaceholder":30},[177,11854,11855,11857,11860],{"class":179,"line":308},[177,11856,9151],{"class":183},[177,11858,11859],{"class":237}," loadData",[177,11861,11639],{"class":187},[177,11863,11864,11866,11869],{"class":179,"line":329},[177,11865,4558],{"class":183},[177,11867,11868],{"class":237}," refresh",[177,11870,717],{"class":187},[177,11872,11873],{"class":179,"line":350},[177,11874,1536],{"class":187},[177,11876,11877,11879,11881],{"class":179,"line":384},[177,11878,11267],{"class":187},[177,11880,11189],{"class":9932},[177,11882,11203],{"class":187},[177,11884,11885],{"class":179,"line":591},[177,11886,218],{"emptyLinePlaceholder":30},[177,11888,11889,11891,11893],{"class":179,"line":613},[177,11890,4143],{"class":187},[177,11892,13],{"class":9932},[177,11894,11203],{"class":187},[177,11896,11897,11899,11901],{"class":179,"line":641},[177,11898,11288],{"class":187},[177,11900,11291],{"class":9932},[177,11902,11203],{"class":187},[177,11904,11905,11907,11910,11913,11915,11918,11921,11923],{"class":179,"line":647},[177,11906,11298],{"class":187},[177,11908,11909],{"class":9932},"button",[177,11911,11912],{"class":237}," @click",[177,11914,984],{"class":187},[177,11916,11917],{"class":194},"\"loadData\"",[177,11919,11920],{"class":187},">Load Data\u003C/",[177,11922,11909],{"class":9932},[177,11924,11203],{"class":187},[177,11926,11927,11929,11931,11934,11936,11939,11942,11944],{"class":179,"line":670},[177,11928,11298],{"class":187},[177,11930,11291],{"class":9932},[177,11932,11933],{"class":237}," v-if",[177,11935,984],{"class":187},[177,11937,11938],{"class":194},"\"pending\"",[177,11940,11941],{"class":187},">Loading...\u003C/",[177,11943,11291],{"class":9932},[177,11945,11203],{"class":187},[177,11947,11948,11950,11952,11954,11956,11959,11962,11964],{"class":179,"line":701},[177,11949,11298],{"class":187},[177,11951,11291],{"class":9932},[177,11953,11933],{"class":237},[177,11955,984],{"class":187},[177,11957,11958],{"class":194},"\"error\"",[177,11960,11961],{"class":187},">{{ error }}\u003C/",[177,11963,11291],{"class":9932},[177,11965,11203],{"class":187},[177,11967,11968,11970,11972,11974,11976,11979,11982,11984],{"class":179,"line":720},[177,11969,11298],{"class":187},[177,11971,11291],{"class":9932},[177,11973,11933],{"class":237},[177,11975,984],{"class":187},[177,11977,11978],{"class":194},"\"data\"",[177,11980,11981],{"class":187},">{{ data }}\u003C/",[177,11983,11291],{"class":9932},[177,11985,11203],{"class":187},[177,11987,11988,11990,11992],{"class":179,"line":725},[177,11989,11313],{"class":187},[177,11991,11291],{"class":9932},[177,11993,11203],{"class":187},[177,11995,11996,11998,12000],{"class":179,"line":738},[177,11997,11267],{"class":187},[177,11999,13],{"class":9932},[177,12001,11203],{"class":187},[177,12003,12004],{"class":179,"line":750},[177,12005,218],{"emptyLinePlaceholder":30},[177,12007,12008,12010,12012,12014],{"class":179,"line":787},[177,12009,4143],{"class":187},[177,12011,10029],{"class":9932},[177,12013,11338],{"class":237},[177,12015,11203],{"class":187},[177,12017,12018],{"class":179,"line":793},[177,12019,12020],{"class":2925},"/* composant styles */\n",[177,12022,12023,12025,12027],{"class":179,"line":798},[177,12024,11267],{"class":187},[177,12026,10029],{"class":9932},[177,12028,11203],{"class":187},[404,12030,12032],{"id":12031},"why-use-these-snippets","Why Use These Snippets?",[12034,12035,12037],"h4",{"id":12036},"enhance-productivity","Enhance Productivity",[17,12039,12040],{},"Raycast snippets save time and effort by providing ready-to-use code templates, allowing you to focus on the unique aspects of your project.",[12034,12042,12044],{"id":12043},"maintain-consistency","Maintain Consistency",[17,12046,12047],{},"Using standardized snippets ensures consistency across your codebase, making it easier to read, maintain, and collaborate on.",[12034,12049,12051],{"id":12050},"streamline-development","Streamline Development",[17,12053,12054],{},"Snippets cater to common development tasks, streamlining your workflow and reducing the likelihood of errors or oversights.",[12034,12056,12058],{"id":12057},"foster-learning","Foster Learning",[17,12060,12061],{},"For new developers or those new to Nuxt and Vue, these snippets offer insight into best practices and efficient coding patterns.",[17,12063,12064],{},"In conclusion, incorporating these Raycast snippets into your Nuxt and Vue development workflow can significantly enhance productivity, maintain code consistency, and streamline your development process. Whether you're building a small project or a large-scale application, these snippets are invaluable tools in the modern developer's arsenal.",[10029,12066,12067],{},"html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sByVh, html code.shiki .sByVh{--shiki-light:#22863A;--shiki-default:#85E89D;--shiki-dark:#85E89D}",{"title":15,"searchDepth":25,"depth":25,"links":12069},[12070,12071],{"id":11126,"depth":25,"text":11127},{"id":11133,"depth":25,"text":11134,"children":12072},[12073,12075,12077,12079,12081,12083],{"id":11156,"depth":215,"text":12074},"Component Template: !comp",{"id":11355,"depth":215,"text":12076},"API Handler Template: !api",{"id":11457,"depth":215,"text":12078},"State Management with Pinia: !store",{"id":11668,"depth":215,"text":12080},"Composable Function Template: !cps",{"id":11751,"depth":215,"text":12082},"Fetching Data with Composition API: !fcomp",{"id":12031,"depth":215,"text":12032},"22/02/2026",{},"/articles/5-amazing-raycast-nuxt-snippets","10",{"title":11113,"description":11123},{"loc":12086},"articles/02.5-amazing-raycast-nuxt-snippets",[12092,12093,12094,12095],"Nuxt","Vue","Raycast","Productivity","zzXLzLCEFUI4KEgfaKL_32pJhTyV22hpPZjfVRfG-jM",{"id":12098,"title":12099,"body":12100,"date":12174,"description":12175,"extension":28,"head":27,"image":12113,"meta":12176,"navigation":30,"ogImage":27,"path":12177,"readingTime":2955,"robots":27,"schemaOrg":27,"seo":12178,"sitemap":12179,"stem":12180,"tags":12181,"__hash__":12184},"articles/articles/03.launch-your-portfolio.md","Finally launch your portfolio",{"type":7,"value":12101,"toc":12163},[12102,12106,12109,12114,12118,12121,12125,12128,12132,12135,12139,12142,12146,12149,12153,12156,12160],[67,12103,12105],{"id":12104},"introduction","Introduction",[17,12107,12108],{},"Creating a portfolio as a development, design or tech professional is a journey punctuated by complex choices. One of the most prevalent of these dilemmas lies in the delicate balance between striving for perfection and the need to launch quickly. Explore with me the nuances of these contradictory perspectives.",[17,12110,12111],{},[45,12112],{"alt":47,"src":12113},"/articles/launch-your-porfolio.jpg",[404,12115,12117],{"id":12116},"the-aspiration-to-perfection-an-unattainable-ideal","The aspiration to perfection: an unattainable ideal?",[17,12119,12120],{},"The traditional vision of artistic creation encourages us to pursue perfection from the outset. Every line of code, every element of design should converge towards absolute excellence. However, this dream of perfection can sometimes become a burden, a brake on progress. The relentless quest for perfection can paralyze the creative process, turning creation into an endless pursuit.",[404,12122,12124],{"id":12123},"the-perfectionism-trap-when-the-best-becomes-the-enemy-of-the-good","The Perfectionism Trap: when the best becomes the enemy of the good",[17,12126,12127],{},"Perfectionism can turn into a subtle trap. Waiting for every detail to be impeccable can delay your portfolio indefinitely. This prolonged stalling can have implications for professional credibility, as the market often demands a fast, dynamic online presence.",[404,12129,12131],{"id":12130},"the-bold-proposal-to-launch-quickly-iterate-rather-than-perfect","The bold proposal to launch quickly: iterate rather than perfect",[17,12133,12134],{},"The idea of launching quickly, even if the portfolio doesn't reach an immediate level of perfection, offers an alternative perspective. It's about recognizing that each iteration can be an improvement on the previous one. This approach encourages a constant iteration mentality, where each version becomes a learning opportunity.",[404,12136,12138],{"id":12137},"speed-as-a-means-of-learning-create-by-doing","Speed as a means of learning: create by doing",[17,12140,12141],{},"Launching quickly thus becomes a means of learning. Speed doesn't mean compromising on quality, but rather accepting that perfection may initially be unattainable. It's an invitation to learn by doing, to use each version of the portfolio as a testing ground.",[404,12143,12145],{"id":12144},"the-crucial-importance-of-time-time-efficiency-and-judicious-management","The crucial importance of time: time efficiency and judicious management",[17,12147,12148],{},"Time plays a crucial role in this debate. Time efficiency, underlined by the perspective of speed, becomes an essential skill. Judicious time management is the key to maintaining momentum without compromising quality. Consciously choosing where to invest one's time becomes a strategic act.",[404,12150,12152],{"id":12151},"total-control-vs-flexibility-navigating-between-the-two-extremes","Total control vs. flexibility: navigating between the two extremes",[17,12154,12155],{},"Total control of the creative process offers a sense of autonomy and empowerment. However, it is vital not to confuse control with rigidity. Being open to market developments and new trends is just as essential as personal control. It's a delicate balance between autonomy and adaptability.",[404,12157,12159],{"id":12158},"the-delicate-balance-a-continuous-journey-of-discovery-and-adaptation","The delicate balance: a continuous journey of discovery and adaptation",[17,12161,12162],{},"Ultimately, portfolio creation is an ongoing journey. It's navigating this paradox with intention. It's finding a balance between the quest for excellence and the need to move quickly, knowing that each iteration is a progression towards an improved version of yourself. To create a portfolio is to embrace the paradox, to consciously choose between perfection and speed, and to accept that the path to excellence is a journey rather than a destination.",{"title":15,"searchDepth":25,"depth":25,"links":12164},[12165],{"id":12104,"depth":25,"text":12105,"children":12166},[12167,12168,12169,12170,12171,12172,12173],{"id":12116,"depth":215,"text":12117},{"id":12123,"depth":215,"text":12124},{"id":12130,"depth":215,"text":12131},{"id":12137,"depth":215,"text":12138},{"id":12144,"depth":215,"text":12145},{"id":12151,"depth":215,"text":12152},{"id":12158,"depth":215,"text":12159},"20/1/2026","Aiming for perfectionism is a very good state of mind. On the other hand, working until perfectionism is 100% can also be bad. Here are some tips to help you get your portfolio off the ground.",{},"/articles/launch-your-portfolio",{"title":12099,"description":12175},{"loc":12177},"articles/03.launch-your-portfolio",[12182,150,12183,12095],"Portfolio","Design","OSzzNP4peb06h7k-_97isAenTWumF4PDOcuqXsU9Ngs",{"id":12186,"title":12187,"body":12188,"date":10047,"description":12218,"extension":28,"head":27,"image":12194,"meta":12219,"navigation":30,"ogImage":27,"path":12220,"readingTime":12221,"robots":27,"schemaOrg":27,"seo":12222,"sitemap":12223,"stem":12224,"tags":12225,"__hash__":12227},"articles/articles/04.not-an-impostor.md","You are not an impostor",{"type":7,"value":12189,"toc":12216},[12190,12195,12198,12201,12204,12207,12210,12213],[17,12191,12192],{},[45,12193],{"alt":47,"src":12194},"/articles/trap-of-perfection.jpg",[17,12196,12197],{},"In the theater of content creation, there are many who, backstage, whisper a phrase tinged with doubt: \"I'm not legitimate, others are so much better\". This melody of insecurity, often played over and over in the minds of emerging creators, is the first act of a much larger work: the conquest of one's own legitimacy.",[17,12199,12200],{},"Imagine yourself standing at the edge of the stage, dazzled by the spotlight of self-judgment. Every creator goes through this. But instead of sinking into the shadows of self-deprecation, take a moment to listen to the whispers of the audience: the other creators. What you hear is not a cacophony of criticism, but a chorus of experiences and lessons.",[17,12202,12203],{},"This is where the magic happens. Instead of seeing the stage as a place for competition, turn it into a space for learning. Let other people's successes inspire you, their mistakes teach you. This is not imitation, but a creative harmony where you can find your own rhythm, your own melody.",[17,12205,12206],{},"Your authenticity is your most precious instrument. Play it with confidence. Every note of your experience, your perspective, resonates in a unique way with your audience. Authenticity is a creator's true opus, far more captivating than the exhausting quest for perfection.",[17,12208,12209],{},"Remember, every creation is a rehearsal for the next. There's no grand finale where everything has to be perfect. It's a continuous concert, where each performance is better than the last.",[17,12211,12212],{},"And in this showroom, you're not alone. Backstage, you'll find plenty of mentors, peers and admirers. They're there to encourage you, to guide you, to applaud your successes and support you in your doubts. This community is your chorus of support, turning fearful solos into courageous duets.",[17,12214,12215],{},"In the end, every curtain raised, every light turned on, is a step closer to accepting your own talent. The feeling of imposture dissipates not when you compare yourself to others, but when you recognize the unique beauty of your own performance. In this room, success is measured not just by the applause at the end, but by the courage to get up on stage and say: \"Here's my story, listen to it\".",{"title":15,"searchDepth":25,"depth":25,"links":12217},[],"In the theater of content creation, many backstage whisper a phrase tinged with doubt: 'I m not legitimate, others are so much better.' This melody of insecurity, often played over and over in the minds of emerging creators, is the first act of a much larger work: the conquest of one's own legitimacy.",{},"/articles/not-an-impostor","3",{"title":12187,"description":12218},{"loc":12220},"articles/04.not-an-impostor",[12226,12095],"Creation","I3Cu6Gr1AptbXBpPDl0DqhFwdZ0HPAYqeqrErmnLTKo",1779819065007]