-
-
-
-
- April 2026
-
- How I set up five specialized AI agents that work together like a small team: one researches,
- one writes code, one audits quality, one hunts falsehoods, and one orchestrates. Real tasks,
- real delegation, real quality control. Here's what actually works and what I wish I knew
- starting out.
-
-
-
-
-
- April 2026
-
- I run a community platform and also build AI tools. The temptation to automate everything
- is real, but communities can smell inauthenticity from miles away. Here's how I use AI
- for spam detection, content research, and i18n while keeping the human touch that
- actually matters.
-
-
-
-
-
- April 2026
-
- Everyone says "self-host your AI for privacy." Few explain what that actually means in
- practice. I deployed an entire AI agent system on a $6/month VPS. Here's the honest
- breakdown of what's private, what isn't, what costs money, and what the tradeoffs are.
-
-
-
-
-
- April 2026
-
- Why would anyone put an AI consulting service on the darknet? Because some clients
- care about anonymity, and I2P (not Tor) is better suited for services, not just
- browsing. Here's how I set up an eepsite and what I learned about serving anonymous
- clients.
-
-
-
+
+
Articles coming soon. Check back later.
@@ -91,6 +48,7 @@
diff --git a/contact.html b/contact.html
index cb0cfda..4bbe14f 100644
--- a/contact.html
+++ b/contact.html
@@ -96,6 +96,7 @@
diff --git a/free-audit.html b/free-audit.html
index cffa96a..7f707d8 100644
--- a/free-audit.html
+++ b/free-audit.html
@@ -193,6 +193,7 @@
diff --git a/index.html b/index.html
index 0dcf959..07993b6 100644
--- a/index.html
+++ b/index.html
@@ -139,6 +139,7 @@
diff --git a/js/main.js b/js/main.js
index 10893ec..2925542 100644
--- a/js/main.js
+++ b/js/main.js
@@ -18,13 +18,11 @@
if (btn) btn.textContent = icons[theme] || '🌙';
}
- // Init: stored preference, then data-theme attr, then default dark
var stored = getStored();
var current = stored || document.documentElement.getAttribute('data-theme') || 'dark';
if (themes.indexOf(current) === -1) current = 'dark';
applyTheme(current);
- // Toggle
document.getElementById('theme-toggle').addEventListener('click', function() {
var idx = themes.indexOf(document.documentElement.getAttribute('data-theme'));
var next = themes[(idx < 0 ? 0 : idx + 1) % themes.length];
@@ -40,17 +38,16 @@ document.getElementById('nav-toggle').addEventListener('click', function() {
});
// Contact form - sends to backend API
-var form = document.getElementById('contact-form');
-if (form) {
- form.addEventListener('submit', function(e) {
+var contactForm = document.getElementById('contact-form');
+if (contactForm) {
+ contactForm.addEventListener('submit', function(e) {
e.preventDefault();
var status = document.getElementById('form-status');
- var submitBtn = form.querySelector('button[type="submit"]');
+ var submitBtn = contactForm.querySelector('button[type="submit"]');
var originalBtnText = submitBtn.textContent;
- // Collect form data
- var formData = new FormData(form);
+ var formData = new FormData(contactForm);
var data = {
name: formData.get('name'),
email: formData.get('email'),
@@ -58,39 +55,31 @@ if (form) {
message: formData.get('message')
};
- // Honeypot for spam
var honeypot = formData.get('website') || formData.get('url') || formData.get('honey');
if (honeypot) {
- // Bot detected - pretend success
status.className = 'notification is-success';
status.textContent = 'Message received. We\'ll respond within 48 hours.';
status.style.display = 'block';
- form.reset();
+ contactForm.reset();
return;
}
- // Disable button and show sending state
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
status.style.display = 'none';
- // Send to API
fetch('/api/contact', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
+ headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
- .then(function(response) {
- return response.json();
- })
+ .then(function(response) { return response.json(); })
.then(function(result) {
if (result.success) {
status.className = 'notification is-success';
status.textContent = result.message || 'Message received. We\'ll respond within 48 hours.';
status.style.display = 'block';
- form.reset();
+ contactForm.reset();
} else {
throw new Error(result.error || 'Failed to send message');
}
@@ -105,4 +94,64 @@ if (form) {
submitBtn.textContent = originalBtnText;
});
});
-}
\ No newline at end of file
+}
+
+// Audit form - sends to backend API
+var auditForm = document.getElementById('audit-form');
+if (auditForm) {
+ auditForm.addEventListener('submit', function(e) {
+ e.preventDefault();
+
+ var status = document.getElementById('audit-status');
+ var submitBtn = auditForm.querySelector('button[type="submit"]');
+ var originalBtnText = submitBtn.textContent;
+
+ var formData = new FormData(auditForm);
+ var data = {
+ name: formData.get('name'),
+ email: formData.get('email'),
+ business: formData.get('business'),
+ audit_type: formData.get('audit_type'),
+ description: formData.get('description')
+ };
+
+ var honeypot = formData.get('website') || formData.get('url') || formData.get('honey');
+ if (honeypot) {
+ status.className = 'notification is-success';
+ status.textContent = 'Audit request received. We\'ll be in touch within 48 hours.';
+ status.style.display = 'block';
+ auditForm.reset();
+ return;
+ }
+
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Submitting...';
+ status.style.display = 'none';
+
+ fetch('/api/audit', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ })
+ .then(function(response) { return response.json(); })
+ .then(function(result) {
+ if (result.success) {
+ status.className = 'notification is-success';
+ status.textContent = result.message || 'Audit request received. We\'ll review your situation and send a report within 48 hours.';
+ status.style.display = 'block';
+ auditForm.reset();
+ } else {
+ throw new Error(result.error || 'Failed to submit audit request');
+ }
+ })
+ .catch(function(error) {
+ status.className = 'notification is-danger';
+ status.textContent = error.message || 'Failed to submit audit request. Please try again.';
+ status.style.display = 'block';
+ })
+ .finally(function() {
+ submitBtn.disabled = false;
+ submitBtn.textContent = originalBtnText;
+ });
+ });
+}
diff --git a/pricing.html b/pricing.html
index d44b522..bf0c3e4 100644
--- a/pricing.html
+++ b/pricing.html
@@ -158,6 +158,7 @@
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 0000000..3a12cc5
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Allow: /
+Disallow: /api/
+
+Sitemap: https://krustyplanet.org/sitemap.xml
diff --git a/scripts/sync-eepsite.sh b/scripts/sync-eepsite.sh
new file mode 100755
index 0000000..174fa54
--- /dev/null
+++ b/scripts/sync-eepsite.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+# sync-eepsite.sh — Copy clearnet site to eepsite with appropriate modifications
+# Usage: ./sync-eepsite.sh [--dry-run]
+#
+# The eepsite is a simplified version of the clearnet site:
+# - No blog page (no blog content yet)
+# - No free-audit page (audit form uses /api which isn't available on I2P)
+# - Cross-reference links point to clearnet (not eepsite) and vice versa
+# - Same JS/CSS
+# - Same footer with I2P/clearnet cross-link
+
+set -euo pipefail
+
+CLEARNET="/var/www/krustyplanet"
+EEPSITE="/var/www/eepsite"
+DRY_RUN=false
+
+if [[ "${1:-}" == "--dry-run" ]]; then
+ DRY_RUN=true
+ echo "DRY RUN — no files will be modified"
+fi
+
+# Pages that exist on both sites
+SHARED_PAGES=("index.html" "services.html" "pricing.html" "contact.html")
+
+# Pages only on clearnet (these have backend API dependencies)
+CLEARNET_ONLY=("blog.html" "free-audit.html")
+
+# Copy shared static assets
+for dir in css js; do
+ echo "Syncing $dir/"
+ if $DRY_RUN; then
+ diff -rq "$CLEARNET/$dir/" "$EEPSITE/$dir/" || true
+ else
+ rsync -av --delete "$CLEARNET/$dir/" "$EEPSITE/$dir/"
+ fi
+done
+
+# Copy favicon
+echo "Syncing favicon.svg"
+if ! $DRY_RUN; then
+ cp "$CLEARNET/favicon.svg" "$EEPSITE/favicon.svg"
+fi
+
+# Sync shared pages with I2P-specific modifications
+for page in "${SHARED_PAGES[@]}"; do
+ echo "Syncing $page"
+ if $DRY_RUN; then
+ echo " Would transform and copy $CLEARNET/$page -> $EEPSITE/$page"
+ else
+ # Start with clearnet version
+ cp "$CLEARNET/$page" "$EEPSITE/$page"
+
+ # Replace clearnet I2P mirror link with clearnet reference link
+ sed -i 's|🔒 I2P Mirror:.*b32\.i2p|🌐 Clearnet:
https://krustyplanet.org|' "$EEPSITE/$page"
+
+ # Remove nav links to clearnet-only pages (free-audit, blog)
+ sed -i '/href="\/free-audit\.html"/d' "$EEPSITE/$page"
+ sed -i '/href="\/blog\.html"/d' "$EEPSITE/$page"
+ fi
+done
+
+echo "Done. Eepsite is in sync with clearnet."
+echo ""
+echo "NOTE: The following pages are clearnet-only (require /api backend):"
+for page in "${CLEARNET_ONLY[@]}"; do
+ echo " - $page"
+done
+echo ""
+echo "NOTE: The eepsite contact form JS still posts to /api/contact"
+echo " which won't work over I2P. Consider adding a mailto: fallback."
diff --git a/services.html b/services.html
index 71d9b7a..1d9ae76 100644
--- a/services.html
+++ b/services.html
@@ -83,6 +83,7 @@
diff --git a/sitemap.xml b/sitemap.xml
new file mode 100644
index 0000000..c9df466
--- /dev/null
+++ b/sitemap.xml
@@ -0,0 +1,33 @@
+
+
+
+ https://krustyplanet.org/
+ weekly
+ 1.0
+
+
+ https://krustyplanet.org/services.html
+ monthly
+ 0.8
+
+
+ https://krustyplanet.org/pricing.html
+ monthly
+ 0.8
+
+
+ https://krustyplanet.org/free-audit.html
+ monthly
+ 0.9
+
+
+ https://krustyplanet.org/contact.html
+ monthly
+ 0.7
+
+
+ https://krustyplanet.org/blog.html
+ weekly
+ 0.6
+
+