-
Talinn, Estonia
-
-
support@lnsolutions.ee

Back in the day I used the now-defunct site coin.fyi or coinexplorers.com to track my portfolio. When they disappeared, I looked at the alternatives — and none of them adapted to my necessities. The main one: a private, encrypted portfolio management platform in the style of ProtonMail. One where the service provider itself cannot see what I hold. So I decided to create my own: Coinwatch.space.
Privacy is something we take for granted, but in today’s digital world it’s actually very rare. Data leaks and “$5 wrench attacks” are unfortunately becoming common. A $5 wrench attack refers to a real-world threat: instead of hacking crypto wallets, criminals simply use physical force or intimidation to steal private keys. As crypto adoption grows, so does this troubling shift from online exploits to offline crime.
I learned this the hard way. When hardware wallet manufacturer Ledger suffered its data breach, over a million email addresses were exposed. Not only was my email leaked — so were my phone number and my home address. I had to incur the cost of moving to another residence, and live with the paranoia of a possible attack. And this keeps happening: as recently as January 2026, Ledger leaked customer personal data again.
Here’s the uncomfortable truth about portfolio trackers: they are an even juicier target than a wallet vendor’s shipping database. A wallet vendor’s leak tells an attacker you probably own crypto. A portfolio tracker’s database tells them exactly how much you own, in which coins, and when you bought it. If that data sits in plaintext on someone’s server, you are one breach away from being on a criminal’s shopping list.
Every tracker I evaluated stored my holdings in plaintext on their servers. Many wanted read access to my exchange accounts via API keys. Some were “free” — which usually means the portfolio data is the product. That was a hard no for me.
What zero-knowledge actually means at Coinwatch.space
The heart of coinwatch.space is the Privacy Vault — client-side, end-to-end encryption modeled on how ProtonMail treats your email:
- Your vault password never leaves your device. It’s run through Argon2id (a memory-hard key derivation function) to derive an AES-256-GCM encryption key — in your browser, not on the server.
- Quantities, prices, cost basis, notes, and loan amounts are stored as ciphertext in the database. If our servers were breached tomorrow, the attacker would get encrypted blobs, not a target list.
- Your browser does the math. The server provides market prices; your device decrypts your holdings locally and computes portfolio value client-side. We never see the result.
- Tax reports and AI sessions are opt-in compute moments. You enter your vault password to authorize a single in-memory session — nothing is persisted in plaintext afterward. Even your AI chat history is encrypted.
- On mobile, the vault password lives in the iOS Keychain / Android Keystore with hardware-backed encryption at rest.
And it goes beyond encryption:
- Sign in without an email address. coinwatch.space supports Nostr authentication — log in with your Nostr key and never hand over an identity at all. No KYC, no mandatory personal data.
- No exchange API keys required. You import your history from the CSV exports your exchange already gives you. I never ask for live access to your accounts.
- DeFi positions are read from public on-chain data (Aave V3, Compound V3) — nothing to hand over there either.
Everything else a serious portfolio tracker should do
Privacy would be pointless if the tool underneath were weak. Coinwatch.space is a full portfolio intelligence platform:
📊Real-time portfolio tracking. Unlimited portfolios, 20+ display currencies, live prices aggregated from major exchanges, 1h / 24h / all-time profit and loss. Import your trade history via CSV from exchanges like Bitfinex, Poloniex, or Wirex — including the messy stuff like split fee rows and credit lines.
🧾Automated, country-specific tax reports. This is where most trackers fall flat. Coinwatch.space generates capital-gains reports built on country-specific rules (Germany, Czech Republic, Slovakia, and more), correctly handling staking rewards, airdrops, and mining income. PDF and CSV export, ready in minutes — and with the Privacy Vault enabled, even your generated reports are stored encrypted. With MiCA now in force across the EU, this went from “nice to have” to mandatory.
🤖 An AI assistant that respects both your money and your privacy. The built-in AI assistant, can analyze your actual holdings, answer market questions, and run tax optimization — finding harvestable losses and lots approaching long-term exemption thresholds. Crucially, all numbers are computed deterministically by the tax engine; the AI explains, it doesn’t guess. It runs on privacy-respecting open models with a prepaid credit system — no subscription lock-in, and your data is never training material. Your AI agent even has a memory and persona you can edit as you like.
🏦 Loan and DeFi tracking with LTV alerts. If you borrow against your crypto (Aave, Compound, or centralized lenders), Coinwatch.space monitors your loan-to-value ratio and warns you before liquidation risk becomes liquidation reality.
🔔 Smart notifications. Price alerts, LTV threshold warnings, and loan payment reminders — delivered by push (iOS/Android) or email.
📰 News that matters to you. A curated feed filtered to the coins you actually hold, aggregated in part over the Nostr network.
🌍 Built for Europe, from day one. The full experience is available in 10 languages — English, German, French, Spanish, Italian, Portuguese, Polish, Czech, Slovak, and even Esperanto.
The trade-off I chose deliberately
Zero-knowledge encryption has a cost: if you lose your vault password, I cannot recover your data. There’s no “reset” that magically decrypts your holdings, because there’s no backdoor. That’s not a bug — it’s the entire point. The same property that locks me out locks out hackers, subpoena-happy data brokers, and anyone who breaches the server.
For me, after seeing my home address in a leaked database, that’s a trade I’ll make every single time.
Try it!
Your portfolio is nobody’s business but yours. If you feel the same way, give coinwatch.space a try — portfolio tracking is free, no email required if you sign in with Nostr, and the Privacy Vault is one click away in settings.
I built the tool I wished existed the day I found my address in someone else’s database. I hope it’s the tool you’ve been looking for too.

In the first part of this series (or here for non subscribers), we built a Bitcoin trading bot that listens to trading signals (for example from a Discord channel) and executes trades on LN Markets. The core pieces were:
In this second part, we’ll evolve that architecture into something more intelligent: a bot that keeps your existing trading strategy logic, but delegates parts of your strategy to a Python-based machine learning (ML) agent.
This is a pattern you can reuse:
In a typical first version of a trading bot, some parameters are hard-coded or stored in a config file:
This is easy to implement but has clear limitations:
The architecture we’ll discuss keeps your discretionary or rule-based strategy intact, but adds a feedback loop:
The result is a bot that is still your strategy, but with a data-informed parameter layer wrapping it.
Let’s zoom out. The evolved system has four main components:
Parses incoming signals.
Decides whether a new trade should be opened or an existing one closed.
Calls LN Markets via ln-markets/api.
Persists trade metadata into trades.db.
trades.db)Stores each trade along with metadata such as indicator, timeframe, trade type, and price information.
Accumulates realized statistics about how trades behave once they are opened.
Python parameter ML agent
Runs periodically (e.g., hourly) in its own virtual environment.
Loads historical trades from trades.db.
Engineers features and trains an ML model.
Writes out a JSON file (e.g., params.json) containing per‑regime parameters.
The Node bot reloads this JSON and uses it as an overlay on top of your existing config.
Conceptually, the pipeline looks like this:
Signals → Node Bot → LN Markets & trades.db
trades.db → Python ML Agent →params.json→ Node Bot (param overlay)
Before bringing ML into the picture, your bot needs to produce the raw material the agent will learn from.
In the Node.js side you already have:
futuresNewTrade).trades.db (ID, timestamps, prices, indicator, timeframe, trade type).To support ML-driven parameter management, add or ensure:
15m, 1h, 4h).Fetches open trades from LN Markets.
Tracks the min and max price each trade sees while it is open.
Persists these “extreme” prices back into the database as simple summary statistics.
This is enough for a Python agent to reconstruct:
The Python agent‘s responsibilities are:
trades.db
def main():
"""Main execution function."""
print("=" * 60)
print("AI Trading Bot Parameter Generator")
print("=" * 60)
# Resolve paths
db_path = Path(__file__).parent / DB_PATH
output_path = Path(__file__).parent / OUTPUT_PATH
if not db_path.exists():
print(f"ERROR: Database not found at {db_path}")
return 1
# Ensure output directory exists
output_path.parent.mkdir(parents=True, exist_ok=True)
# Load data
df = load_trade_data(str(db_path))
2. Engineer features: Typical features include:
Indicator.
Timeframe.
Trade type (long vs short).
Regime flags (e.g., a boolean for trending vs ranging).
Distance to a moving average at entry.
Whether the entry was near a recent local high/low.
Simple time-of-day or day-of-week information.
def engineer_features(df):
"""Engineer features for ML model."""
# Create derived features
df['ma_distance'] = np.where(
df['movingAverage'].notna() & (df['entryprice'] != 0),
(df['entryprice'] - df['movingAverage']) / df['entryprice'],
0
)
df['local_bottom_distance'] = np.where(
df['localBottom'].notna() & (df['entryprice'] != 0),
(df['entryprice'] - df['localBottom']) / df['entryprice'],
0
)
df['local_top_distance'] = np.where(
df['localTop'].notna() & (df['entryprice'] != 0),
(df['localTop'] - df['entryprice']) / df['entryprice'],
0
)
print(f"Engineered features for {len(df)} trades")
return df
3. Train an ML model: The agent can use a tree-based regressor such as XGBoost (or any library in your stack) to learn a mapping from:
(indicator, timeframe, regime, trade type, features)def train_model(df):
"""Train XGBoost model """
print("Training XGBoost model...")
# Define target and features
target = 'sl_pct'
exclude_cols = [
target,
'entryprice',
'stoploss',
'takeprofit',
'profit',
'movingAverage',
'localBottom',
'localTop',
'features',
]
feature_cols = [col for col in df.columns if col not in exclude_cols]
X = df[feature_cols]
y = df[target]
# Split data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train model
model = XGBRegressor(
n_estimators=100,
max_depth=5,
learning_rate=0.1,
random_state=42,
objective='reg:squarederror'
)
model.fit(X_train, y_train)
# Evaluate
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print(f"Model trained successfully!")
print(f" MAE: {mae:.4f}%")
print(f" RMSE: {rmse:.4f}%")
print(f" Feature importance (top 5):")
feature_importance = sorted(
zip(feature_cols, model.feature_importances_),
key=lambda x: x[1],
reverse=True
)[:5]
for feat, importance in feature_importance:
print(f" {feat}: {importance:.4f}")
return model, feature_cols
4. Aggregate by regime and write JSON: Rather than storing one prediction per trade, the agent:
(indicator, timeframe, regime flag, trade type).The output is written to something like:
analysis/params.jsonStructured as a nested object keyed by:
Each leaf contains statistics and recommended parameters.
Again, this is your design: you decide what “parameter” means; the pattern here is only about how to export and consume it.
On the Node.js side, you integrate the Python agent as a background job.
stdout / stderr.This keeps the ML agent separate from your trading loop. The trading bot only cares that a JSON file appears when training succeeds.
2. Schedule periodic retraining: Using a scheduler like node-cron, you can:
Reload the params.json file into memory.
With params.json successfully loaded, the Node.js bot can expose a couple of helper functions that act as a thin translation layer between:
Typical patterns:
Then:
params.json.This ensures backwards compatibility: you can always turn off the ML overlay or let it “fill in” only for regimes with enough data.
Whenever you plug ML into a live trading system, you need guardrails. Some important patterns:
These patterns are generic and apply to any trading strategy, not just the one in this bot.
In Part 1 we focused on connecting a Node.js bot to LN Markets and executing trades from external signals. In this Part 2, we extended that architecture with a Python ML agent that learns from your historical trades and feeds the bot with regime-aware parameters via a simple JSON interface.
The critical design choices were:
trades.db as the shared source of truth.From here, you can extend the same pattern to:
All of this leverages machine learning and Python AI agents to make your trading bot more adaptive and data-driven.
The XRechnung LNSolutions addon extends Alfresco with full support for German XRechnung electronic invoices. It automatically detects XRechnung XML documents, enriches them with metadata, and generates high-quality HTML and PDF renditions via a dedicated Transform Engine (T‑Engine).
The solution consists of three main components:
xr:xrechnungMetadata, and integration with the T‑Engine. WEB-INF/lib or custom Docker image)To request a trial license, use:
Request XRechnung trial license
lnsolutions-alfresco-license-1.0.0.jar to:
<TOMCAT>/webapps/alfresco/WEB-INF/lib/ (non-Docker)xrechnung-lnsolutions-platform-1.0-SNAPSHOT.jar to:
<TOMCAT>/webapps/alfresco/WEB-INF/lib/ (non-Docker)lnsolutions-alfresco-license).In alfresco-global.properties (or equivalent Docker environment), add the XRechnung T‑Engine URL:
# URL of the XRechnung Transform Engine
localTransform.xrechnung.url=http://xrechnung-tengine:8091
Adjust the host/port to your environment (e.g. http://localhost:8091 for standalone testing).
docker run -d -p 8091:8091 --name xrechnung-tengine xrechnung-tengine:latest
curl http://localhost:8091/ready
java -jar xrechnung-tengine-1.0-SNAPSHOT.jar
curl http://localhost:8091/ready
Configure the following environment variables for the T‑Engine (e.g. in a .env file or service configuration):
XRECHNUNG_TRANSFORM_CONFIG=/opt/xrechnung-tengine/config/xrechnung-transform-config.json
CORE_AIO_TRANSFORM_URL=http://transform-core-aio:8090/transform
LICENSE_VALIDATION_URL=http://<alfresco-host>:8080/alfresco/service/lnsolutions/admin/license-status
The T‑Engine calls LICENSE_VALIDATION_URL before each transform to enforce your license via Alfresco.
xr:xrechnungMetadata).Digital Invoice Profile, Digital Invoice Profile Description (Short), and Is Digital Invoice.http://<alfresco-host>:8080/alfresco/service/lnsolutions/node/xrechnung-html?id={nodeId}

The addon also supports a PDF pipeline: XRechnung XML → HTML (T‑Engine) → PDF (Alfresco pdfRenderer).

The XRechnung addon uses the XRechnung Metadata aspect (xr:xrechnungMetadata) as the technical indicator that a document is a valid XRechnung invoice.
xr:isDigitalInvoice): Boolean flag that is set when a document is detected as XRechnung.xr:digitalInvoiceProfile): Stores the detected digital invoice profile identifier.xr:digitalInvoiceProfileDescShort): Short, human‑readable label for the detected profile (e.g. displayed as “Digital Invoice Profile Description (Short)” in Alfresco).The XRechnungDetectionBehavior listens to document creation and updates for cm:content nodes. When an uploaded XML file matches the XRechnung patterns (based on its content, metadata, or filename), the behavior automatically adds the xr:xrechnungMetadata aspect and sets xr:isDigitalInvoice = true.
Only documents that carry this XRechnung indicator (the xr:xrechnungMetadata aspect with xr:isDigitalInvoice=true) are treated as XRechnung by the HTML/PDF transformation pipeline. Generic XML files remain unaffected until they are positively identified as XRechnung.

If you want to evaluate the XRechnung addon before purchasing a full license, request a trial here:
Request XRechnung Trial License
Or contact us for a permanent license.
Read more on Medium
Git is a powerful version control system widely used by developers to manage source code repositories. One of the key features of Git is its support for branching, allowing developers to work on new features or experiments without affecting the main codebase. However, managing branches and resolving merge conflicts effectively are crucial skills for successful collaboration and code management. In this article, we’ll explore common Git branching tasks and how to handle merge conflicts gracefully for feature branches.
Git branches are independent lines of development within a Git repository. They allow developers to work on different features, bug fixes, or experiments without affecting the main codebase. Here are some essential concepts related to Git branches:
git checkout -b <branch-name> command. This creates a new branch and switches to it in one step.git checkout <branch-name> command. This allows you to work on different features or bug fixes seamlessly.git branch command. The branch you are currently on will be highlighted.git branch -d <branch-name> command. Be cautious when deleting branches, especially if they contain unmerged changes.Merge conflicts occur when Git cannot automatically merge changes from different branches due to conflicting changes in the same part of a file. Here’s how to handle merge conflicts effectively:
git pull origin <branch-name>.git merge <branch-name> command. Git will attempt to automatically merge the changes. If there are conflicts, Git will pause the merge process.<<<<<<<, =======, >>>>>>>) and keep the changes you want to retain.git add . and commit the merge using git commit. Git will create a merge commit to finalize the merge process.
In this example we start with main and then create a new local branch and switch to it
Read more on Medium
Read on Medium
Discord has become a cornerstone platform for communication among gamers, communities, and teams worldwide. The extensibility of Discord through bots has unlocked a plethora of possibilities, enabling automation, moderation, and enhanced user experiences. In this guide, we’ll delve into Discord bot development, using LN Markets as trading platform.
Trading signals Discord channels and groups are online communities where traders come together to share valuable insights, trading signals, and analyses pertaining to the crypto market. These channels and groups are hosted on the Discord messaging platform, enabling seamless real-time communication and information exchange among members.
LN Markets is a platform that enables users to trade Bitcoin futures contracts using the Lightning Network, a second-layer solution for faster and cheaper Bitcoin transactions. LN Markets leverages the Lightning Network’s micro-payment capabilities to provide instant settlement and low transaction fees for trading activities.
Overall, LN Markets provides a unique trading experience for Bitcoin enthusiasts and traders looking to participate in the cryptocurrency markets using the Lightning Network’s innovative technology.
// Import necessary modules
const Discord = require('discord.js');
const client = new Discord.Client();
// Define bot behavior
client.on('message', message => {
if (message.content === '!hello') {
message.reply('Hello, world!');
}
});
// Log in to Discord with your bot token
client.login('YOUR_DISCORD_BOT_TOKEN');
Here we will just focus on reading the last message from the trading channel
// Register event for when client receives a message.
client.on('message', message => {
console.log(message.content);
processSignal(message);
});
We need to install the ln-markets api for our project via this command:
npm install @ln-markets/api
The next code snippet will open a connection to the LN Markets API.
import { createRestClient } from '@ln-markets/api';
const key = config.lnmarketsKey.trim();
const secret = config.lnmarketsSecret.trim();
const passphrase = config.lnmarketsPassphrase.trim();
const network = 'testnet';
const lnclient = createRestClient({ key, secret, passphrase });
Now we parse the text from the Discord message and trade accordingly. In this example the signal didn’t come in plain text, but it used some rich text features from Discord (message.embeds), so these fields had to be parsed and not only the message.text field.
async function processSignal(message) {
if (message.embeds.length > 0 && message.embeds[0].title.includes('Buy Signal!')) {
// Extract trade information
let entryPrice = null;
let stopLoss = null;
let takeProfit = null;
// Split message content by lines
const createdTimestamp = message.createdTimestamp;
const lines = message.embeds[0].fields;
lines.forEach((line, index) => {
if (line.name.includes('Entry Price')) {
const priceLine = line.value.replace(/`/g, '');
entryPrice = parseFloat(priceLine);
} else if (line.name.includes('Stop Loss')) {
const priceLine = line.value.replace(/`/g, '');
stopLoss = Math.round(parseFloat(priceLine)) + 0.5;
} else if (!line.value.includes('There will be a separate signal to take profit') && line.name.includes('Take Profit')) {
const priceLine = line.value.replace(/`/g, '');
takeProfit = Math.round(parseFloat(priceLine)) + 0.5;
} else if (line.value.includes('There will be a separate signal to take profit that could be higher than this price.')) {
const regex = /```(\d+(\.\d+)?)```/;
// Extract the numeric value using the match method and regular expression
const match = line.value.match(regex);
if (match) {
// Extract the numeric value from the matched string
const priceLine = parseFloat(match[1]);
takeProfit = Math.round(parseFloat(priceLine)) + 0.5;
}
}
});
if (entryPrice !== null && stopLoss !== null) {
console.log(`Entry Price: ${entryPrice}`);
console.log(`Stop Loss: ${stopLoss}`);
if (takeProfit !== null) {
console.log(`Take Profit: ${takeProfit}`);
}
await getBuyTrade(createdTimestamp, takeProfit, stopLoss, entryPrice);
}
} else if (message.embeds.length > 0 && message.embeds[0].title.includes('Sell Signal!')) {
let sellPrice = null;
const createdTimestamp = message.createdTimestamp;
const lines = message.embeds[0].fields;
lines.forEach((line, index) => {
if (line.value.includes('position has been closed at:')) {
const regex = /```(\d+(\.\d+)?)```/;
// Extract the numeric value using the match method and regular expression
const match = line.value.match(regex);
if (match) {
// Extract the numeric value from the matched string
const priceLine = parseFloat(match[1]);
sellPrice = parseFloat(priceLine);
}
}
});
if (sellPrice !== null) {
console.log(`Sell Price: ${sellPrice}`);
await getTradeByTimestamp(createdTimestamp, (err, rows) => {
if (err) {
console.error(err.message);
} else {
// Check if any rows were returned
if (rows.length > 0) {
// Assuming id is the first column in the result set
const tradeId = rows[0].id;
console.log('Trade ID in Sell:', tradeId);
} else if (lastBuyTradeId != null) { // This is a new sell trade
// Perform Sell Trade in LNMarkets
const data = {
id: lastBuyTradeId,
};
console.log(data);
const trade = lnclient.futuresCloseTrade(lastBuyTradeId).then((response) => {
saveToDatabase(response.id, createdTimestamp, 0.0, 0.0, sellPrice, response.pl, 'Sell');
})
.catch((err) => {
// Handle error
console.error(err.message);
});
}
}
});
}
}
}
async function getBuyTrade(createdTimestamp, takeProfit, stopLoss, entryPrice) {
try {
// Call getTradeByTimestamp and wait for it to complete
const rows = await new Promise((resolve, reject) => {
getTradeByTimestamp(createdTimestamp, (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
// Check if any rows were returned
if (rows.length > 0) {
const tradeId = rows[0].id;
lastBuyTradeId = tradeId;
console.log('Trade ID in Buy:', tradeId);
} else {
// Perform Trade in LNMarkets
let data = {
"type": 'm',
"side": 'b',
"leverage": LEVERAGE,
"quantity": QUANTITY,
"takeprofit": takeProfit,
"stoploss": stopLoss
};
const trade = await lnclient.futuresNewTrade(data);
saveToDatabase(trade.id, createdTimestamp, entryPrice, stopLoss, takeProfit, 0.0, 'Buy');
}
} catch (error) {
console.error(error.message);
}
In the previous code snippet, the function getBuyTrade uses getTradeByTimestamp, which queries a local sqlite database file to query if this trade was already done. The function saveToDatabase saves the trade to this database when the trade is new. This in order to execute each trade only once.
function saveToDatabase(lnMarketsId, createdTimestamp, entryPrice, stopLoss, takeProfit, profit, tradeType) {
lastBuyTradeId = lnMarketsId;
createTrade(lnMarketsId, createdTimestamp, entryPrice, stopLoss, takeProfit, profit, tradeType, err => {
if (err) {
console.error(err.message);
} else {
console.log(tradeType + " Trade created successfully!");
}
});
}
Finally we add some cleanup code for our database connection
// Clean up database connection when the application exits
process.on('exit', () => {
closeConnection();
console.log('Database connection closed');
});
// Handle Ctrl+C (SIGINT) signal
process.on('SIGINT', () => {
closeConnection();
console.log('Database connection closed');
process.exit();
});
In this guide we saw an introduction on how to program a Bitcoin trading bot on LN Markets, based on trading signals sent to a specific Discord channel. By mastering the concepts and techniques outlined in this guide, you’ll be well-equipped to build bots that take signals on specific trading servers and automate your trading