├── .gitignore ├── api-permissions.png ├── package.json ├── commands ├── withdraw.js └── stack.js ├── LICENSE ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | stacksats.sh 4 | -------------------------------------------------------------------------------- /api-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennisreimann/stacking-sats-kraken/HEAD/api-permissions.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "stacking-sats-kraken", 4 | "version": "0.4.5", 5 | "description": "Use the Kraken API to stack sats", 6 | "author": "Dennis Reimann ", 7 | "homepage": "https://github.com/dennisreimann/stacking-sats-kraken", 8 | "bugs": "https://github.com/dennisreimann/stacking-sats-kraken/issues", 9 | "funding": { 10 | "url": "https://github.com/dennisreimann/stacking-sats-kraken?sponsor=1" 11 | }, 12 | "license": "MIT", 13 | "main": "stack.js", 14 | "scripts": { 15 | "stack": "node index.js --cmd=stack", 16 | "withdraw": "node index.js --cmd=withdraw", 17 | "test:stack": "node index.js --cmd=stack --validate", 18 | "test:withdraw": "node index.js --cmd=withdraw --validate" 19 | }, 20 | "dependencies": { 21 | "kraken-api": "1.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /commands/withdraw.js: -------------------------------------------------------------------------------- 1 | module.exports = async (kraken, validate, { getEnv }) => { 2 | const [maxFee, key] = getEnv('KRAKEN_MAX_REL_FEE', 'KRAKEN_WITHDRAW_KEY') 3 | 4 | // https://api.kraken.com/0/private/WithdrawInfo 5 | const asset = 'XBT' 6 | const withdrawdetails = { asset, key, amount: 0 } 7 | 8 | // Get withdrawal information 9 | const { result: { limit, fee } } = await kraken.api('WithdrawInfo', withdrawdetails) 10 | const relFee = 1/parseFloat(limit)*parseFloat(fee) 11 | 12 | console.log(`💡 Relative fee of withdrawal amount: ${(relFee*100).toFixed(2)}%`) 13 | 14 | // Place withdrawal when fee is low enough (relatively) 15 | if (relFee < maxFee/100) { 16 | console.log(`💰 Withdraw ${limit} ${asset} now.`) 17 | const withdraw = { asset, key, amount: limit } 18 | if (!validate) { 19 | const { result: { refid } } = await kraken.api('Withdraw', withdraw) 20 | if (refid) console.log(`📎 Withdrawal reference ID: ${refid}`) 21 | } 22 | } else { 23 | console.log(`❌ Fee is too high - max rel. fee: ${parseFloat(maxFee).toFixed(2)}%`) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Dennis Reimann (https://dennisreimann.de) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const Kraken = require('kraken-api') 3 | 4 | const getEnv = (...vars) => vars.map(name => { 5 | const value = process.env[name] 6 | assert(value, `Provide the ${name} environment variable.`) 7 | return value 8 | }) 9 | const getEnvOpt = (varname, defaultValue, allowedValues) => { 10 | const value = process.env[varname] || defaultValue 11 | if (allowedValues) assert(allowedValues.includes(value), `The ${varname} environment variable must be one of ${allowedValues.map(v => `"${v}"`).join(", ")}.`) 12 | return value 13 | } 14 | const command = process.argv[2].replace('--cmd=', '') 15 | const validate = process.argv.includes('--validate') || process.env['KRAKEN_DRY_RUN_PLACE_NO_ORDER'] 16 | 17 | ;(async () => { 18 | try { 19 | const [apiKey, secret] = getEnv('KRAKEN_API_KEY', 'KRAKEN_API_SECRET') 20 | const kraken = new Kraken(apiKey, secret, { timeout: 30000 }) 21 | 22 | const cmd = require(`./commands/${command}`) 23 | await cmd(kraken, validate, { getEnv, getEnvOpt }) 24 | 25 | if (validate) console.log('\n🚨 THIS WAS JUST A VALIDATION RUN!') 26 | } catch (err) { 27 | console.log(`\n🚨 Failure:`, err.message) 28 | } 29 | })() 30 | -------------------------------------------------------------------------------- /commands/stack.js: -------------------------------------------------------------------------------- 1 | module.exports = async (kraken, validate, { getEnv, getEnvOpt }) => { 2 | const [fiat, amount, feeCurrency] = getEnv('KRAKEN_API_FIAT', 'KRAKEN_BUY_AMOUNT', 'KRAKEN_FEE_CURRENCY') 3 | const ratio = getEnvOpt('KRAKEN_BUY_RATIO', '1.0') 4 | const ordertype = getEnvOpt('KRAKEN_ORDER_TYPE', 'limit', ['limit', 'market']) 5 | // if living in Germany, one needs to add an additional parameter to explicitly agree to the trade 6 | // if the parameter is not set one will get the following error: EOrder:Trading agreement required 7 | // see https://support.kraken.com/hc/en-us/articles/360000920026--Trading-agreement-required-error-for-German-residents 8 | const trading_agreement = getEnvOpt('KRAKEN_GERMANY_TRADING_AGREEMENT', '', ['agree', '']) 9 | 10 | // https://www.kraken.com/features/api 11 | const crypto = 'XBT' 12 | const pair = `${crypto}${fiat}` 13 | 14 | // for explanation of oflags see https://www.kraken.com/features/api#add-standard-order 15 | var fee = "" 16 | if (feeCurrency == crypto) { 17 | fee = "fcib" 18 | } else if (feeCurrency == fiat) { 19 | fee = "fciq" 20 | } else { 21 | fee = "" 22 | } 23 | 24 | // Fetch and display information 25 | const { result: balance } = await kraken.api('Balance') 26 | const { result: ticker } = await kraken.api('Ticker', { pair }) 27 | 28 | const fiatBalance = balance[`Z${fiat}`] || balance[fiat] || 0.0 29 | const cryptoBalance = balance[`X${crypto}`] || balance[crypto] || 0.0 30 | const [{ a: [a], b: [b] }] = Object.values(ticker) 31 | const ask = parseFloat(a) 32 | const bid = parseFloat(b) 33 | const price = Math.round( bid * ratio * 10 ) / 10 34 | 35 | // Calculate volume and adjust precision 36 | const volume = (amount / price).toFixed(8) 37 | 38 | console.log('💰 Balance:', fiatBalance, fiat, '/', cryptoBalance, crypto, '\n') 39 | console.log('📈 Ask:', ask, fiat) 40 | console.log('📉 Bid:', bid, fiat, '(', ( ratio * 100 ), '%', ')') 41 | console.log('🏷️ Price:', price, fiat, "/", "XBT") 42 | console.log('\n') 43 | 44 | if (parseFloat(fiatBalance) < parseFloat(amount)) { 45 | console.log('❌ Insufficient funds') 46 | return 47 | } 48 | 49 | // Place order 50 | const details = { pair, type: 'buy', ordertype, price, volume } 51 | if (validate) details.validate = true 52 | if (trading_agreement) details.trading_agreement = trading_agreement 53 | if (fee) details.oflags = fee 54 | 55 | const { result: { descr: { order }, txid } } = await kraken.api('AddOrder', details) 56 | 57 | console.log('💸 Order:', order) 58 | if (txid) console.log('📎 Transaction ID:', txid.join(', ')) 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stacking Sats on Kraken 2 | 3 | First off: Here's to you, [Bittr](https://getbittr.com/) – you will be missed! 😢 4 | 5 | This script is not a full replacement for the incredible service Bittr offered, but it's a start: 6 | Automate your Stacking Sats process by regularly placing buy orders using the [Kraken API](https://www.kraken.com/features/api). 7 | 8 | --- 9 | 10 | The original [Bittr is back](https://getbittr.com/press-releases/bittr-is-back)! 11 | This script is still fully working. 12 | 13 | --- 14 | 15 | ## ✋ Caveat 16 | 17 | You need to install the dependency [kraken-api](https://github.com/nothingisdead/npm-kraken-api), which is a third-party package. 18 | It has a minimal set of dependencies and I've done my best to audit its code. 19 | Also the version is fixed, so that unwanted changes do not slip in. 20 | 21 | However: Use this at your own risk and decide for yourself whether or not you want to run this script and its dependencies! 22 | 23 | ## 🔑 API Key 24 | 25 | Obtain your Kraken API Key via [the API settings page](https://www.kraken.com/u/security/api). 26 | Generate a new API key dedicated for stacking using the "Query Funds" and "Create and modify orders" permissions: 27 | 28 | ![Kraken API Key Permissions](./api-permissions.png) 29 | 30 | Only check the "Withdraw Funds" option if you plan to automatically withdraw Bitcoin from Kraken. 31 | See details below. 32 | 33 | ## ⚙️ Configuration 34 | 35 | These are the environment variables used by the script: 36 | 37 | ```sh 38 | # used to authenticate with Kraken 39 | KRAKEN_API_KEY="apiKeyFromTheKrakenSettings" 40 | KRAKEN_API_SECRET="privateKeyFromTheKrakenSettings" 41 | 42 | # used for buying 43 | KRAKEN_ORDER_TYPE="market" # "limit" (default) or "market" 44 | KRAKEN_API_FIAT="USD" # the governmental shitcoin you are selling 45 | KRAKEN_BUY_AMOUNT=21 # fiat amount you trade for the future of money 46 | KRAKEN_FEE_CURRENCY="XBT" # pay fee in this currency, e.g. buying XBT for USD and paying fee in XBT 47 | #KRAKEN_BUY_RATIO=0.99 # place order at 99% of current bid price, e.g. to ensure maker fees 48 | 49 | # used for withdrawal 50 | KRAKEN_MAX_REL_FEE=0.5 # maximum fee in % that you are willing to pay 51 | KRAKEN_WITHDRAW_KEY="descriptionOfWithdrawalAddress" 52 | 53 | # set this if you live in Germany and get the 'EOrder:Trading agreement required' error 54 | # see https://support.kraken.com/hc/en-us/articles/360000920026--Trading-agreement-required-error-for-German-residents 55 | KRAKEN_GERMANY_TRADING_AGREEMENT="agree" 56 | 57 | # remove this line after verifying everything works 58 | KRAKEN_DRY_RUN_PLACE_NO_ORDER=1 59 | ``` 60 | 61 | ## ⚡️ RaspiBlitz Integration 62 | 63 | This script ships with the [RaspiBlitz](https://github.com/rootzoll/raspiblitz) (v1.6 and above). 64 | 65 | You can enable it via the Console of your RaspiBlitz. 66 | Leave the main menu via the last option "Console" and use the following commands: 67 | 68 | ```sh 69 | # enable the script 70 | ./config.scripts/bonus.stacking-sats-kraken.sh on 71 | 72 | # switch to the stackingsats user 73 | sudo su - stackingsats 74 | 75 | # edit your configuration (see the "Configuration" section above) 76 | nano /mnt/hdd/app-data/stacking-sats-kraken/.env 77 | 78 | # follow the instructions from the first step to set up a cronjob 79 | crontab -e 80 | ``` 81 | 82 | Here is an example for a daily cronjob at 6:15am ... 83 | 84 | ```sh 85 | SHELL=/bin/bash 86 | PATH=/bin:/usr/sbin:/usr/bin:/usr/local/bin 87 | 15 6 * * * /home/stackingsats/stacking-sats-kraken/stacksats.sh > /dev/null 2>&1 88 | ``` 89 | 90 | **Note:** Do not run `npm` directly on the RaspiBlitz, like show in the examples below. 91 | Please use the `/home/stackingsats/stacking-sats-kraken/stacksats.sh` shell script instead, as this loads your configuration. 92 | On RaspiBlitz (v1.8 and above) you may use different configuration files for e.g. family or business. 93 | 94 | To run the script manually, switch to the `stackingsats` user and use this command: 95 | 96 | ```sh 97 | # switch to the stackingsats user 98 | sudo su - stackingsats 99 | 100 | # run the script 101 | ./stacking-sats-kraken/stacksats.sh 102 | ``` 103 | 104 | - - - 105 | 106 | ## 📦 Custom Setup 107 | 108 | Prerequisite: At least the current LTS version of [Node.js](https://nodejs.org/). 109 | 110 | Install the dependencies: 111 | 112 | ```sh 113 | npm install 114 | ``` 115 | 116 | Setup the environment variables for the script. 117 | See the [config section above](#-configuration). 118 | 119 | Use a dry run to test the script and see the output without placing an order: 120 | 121 | ```sh 122 | npm run test:stack 123 | ``` 124 | 125 | You should see something like this sample output: 126 | 127 | ```text 128 | 💰 Balance: 210000.00 USD / 21.0 XBT 129 | 130 | 📈 Ask: 21000.2 USD 131 | 📉 Bid: 21000.1 USD 132 | 133 | 💸 Order: buy 0.21212121 XBTUSD @ limit 21000.1 134 | 📎 Transaction ID: 2121212121 135 | ``` 136 | 137 | ## 🤑 Stack sats 138 | 139 | When you are good to go, execute this command in a regular interval: 140 | 141 | ```sh 142 | npm run stack 143 | ``` 144 | 145 | The best and easiest way is to wrap it all up in a shell script. 146 | This script can be triggered via cron job, e.g. weekly, daily or hourly. 147 | 148 | Here's a sample `stacksats.sh` script: 149 | 150 | ```sh 151 | #!/bin/bash 152 | set -e 153 | 154 | export KRAKEN_API_KEY="apiKeyFromTheKrakenSettings" 155 | export KRAKEN_API_SECRET="privateKeyFromTheKrakenSettings" 156 | export KRAKEN_ORDER_TYPE="market" 157 | export KRAKEN_API_FIAT="USD" 158 | export KRAKEN_BUY_AMOUNT=21 159 | export KRAKEN_MAX_REL_FEE=0.5 160 | export KRAKEN_WITHDRAW_KEY="descriptionOfWithdrawalAddress" 161 | export KRAKEN_DRY_RUN_PLACE_NO_ORDER=1 162 | export KRAKEN_BUY_RATIO=0.99 163 | 164 | # run script 165 | cd $(cd `dirname $0` && pwd) 166 | cmd=${1:-"stack"} 167 | 168 | if [[ "${KRAKEN_DRY_RUN_PLACE_NO_ORDER}" ]]; then 169 | result=$(npm run test:$cmd --silent 2>&1) 170 | else 171 | result=$(npm run $cmd --silent 2>&1) 172 | fi 173 | echo "$result" 174 | 175 | # optional: send yourself an email 176 | recipient="satstacker@example.org" 177 | echo "Subject: Sats got stacked 178 | From: satstacker@example.org 179 | To: $recipient $result" | /usr/sbin/sendmail $recipient 180 | ``` 181 | 182 | Make it executable with `chmod +x stacksats.sh` and go wild. 183 | 184 | [Stay humble!](https://twitter.com/matt_odell/status/1117222441867194374) 🙏 185 | 186 | ## 🔑 Withdrawal 187 | 188 | Holding significant amounts on an exchange is never a good idea. 189 | You should regularly take ownership of your coins by withdrawing to your own wallet. 190 | This can either be done manually or it can be automated. 191 | The script provided here will only withdraw to a previously defined Bitcoin address if the relative fees do not exceed a certain limit. 192 | 193 | *It is optional to run the withdrawal script.* 194 | 195 | ### Example 1 196 | 197 | - Max. relative fee: 0.5% 198 | - Fixed Kraken fee: ₿ 0.00050 199 | - Balance: ₿ 0.06000 200 | ➡️ No withdrawal since fee actual (0.83%) is too high 201 | 202 | ### Example 2 203 | 204 | - Max. relative fee: 0.5% 205 | - Fixed Kraken fee: ₿ 0.00050 206 | - Balance: ₿ 0.12000 207 | ➡️ Withdrawal executed since actual fee (0.42%) is low enough 208 | 209 | In case you plan to automatically withdraw from Kraken, a withdrawal method must first be defined. 210 | If you already set up a methode you can reuse it. 211 | Otherwise generate a new one by going to **Funding > Bitcoin (XBT) withdraw > Add address**. 212 | The description field will later be used as an environment variable in the script. 213 | 214 | To test the withdrawal of funds to your defined address run: 215 | 216 | ```sh 217 | npm run test:withdraw 218 | ``` 219 | 220 | You should see something like this: 221 | 222 | ```text 223 | 💡 Relative fee of withdrawal amount: 5.57% 224 | ❌ Fee is too high – max rel. fee: 0.50% 225 | ``` 226 | 227 | It is recommended to run the withdrawal script every time you stacked sats: 228 | 229 | ```sh 230 | npm run withdraw 231 | ``` 232 | 233 | Since it can take a couple seconds or minutes for your order to fill, you should run the following script a couple hours later after the stacking script. 234 | Just set up a second cron job which executes the withdrawal script. 235 | 236 | If you are using the aforementioned `stacksats.sh` script you can withdraw via this command: 237 | `stacksats.sh withdraw` 238 | --------------------------------------------------------------------------------