But you see, it’s now [checks time] 2026, so you don’t have to learn something to do something. We have robots.
This is what the robot produced during my 20 min commute to work this morning, over the voice (!) chat. I haven’t tested this, but I’m sure with minor massaging it can be made to work.
:warning: BELOW IS AN UNADULTERATED AND UNAUDITED AI VOIMT :WARNING: USE IT AT YOUR OWN RISK :SKULL: NO HUMAN MEANINGFULLY REVIEWED THIS
Below is the complete deployment guide for an automated Storj → USD market sell on Coinbase, including the single-file Python script, webhook setup, and optional Caddy HTTPS termination.
Storj Auto-Sell to USD — Complete Guide
Overview
You will:
-
Run a Python webhook listener that:
- Receives Coinbase on-chain deposit notifications
- Verifies webhook signatures
- Immediately places a market sell order via Coinbase Advanced Trade API
-
Expose it to the internet via valid HTTPS
- Either with Cloudflare Tunnel (already solved for you)
- Or with Caddy + Let’s Encrypt (covered here)
Requirements
- Public HTTPS endpoint for webhook (Cloudflare or Caddy)
- Coinbase account with API keys (with trade permission)
- Python 3.9+ installed
Environment Variables
COINBASE_API_KEY
COINBASE_API_SECRET
WEBHOOK_LISTENER_SECRET # from subscription creation
STORJ_CONTRACT_ADDRESS # STORJ ERC-20 contract
WATCHED_WALLET_ADDRESS # your deposit address
WEBHOOK_HOST # https://your.domain
Set these before running the Python script.
Single Python Script — storj_autosell.py
#!/usr/local/bin/python3
import os, sys, time, json, uuid, hmac, hashlib, base64, logging
from typing import Optional
import requests
from flask import Flask, request, abort
# ───────────────────────── Logging ─────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
log = logging.getLogger("storj-autosell")
# ───────────────────────── Environment ─────────────────────
required_env = [
"COINBASE_API_KEY",
"COINBASE_API_SECRET",
"WEBHOOK_LISTENER_SECRET",
"STORJ_CONTRACT_ADDRESS",
"WATCHED_WALLET_ADDRESS",
"WEBHOOK_HOST",
]
missing = [e for e in required_env if not os.environ.get(e)]
if missing:
log.critical("missing env vars: %s", missing)
sys.exit(1)
API_KEY = os.environ["COINBASE_API_KEY"]
API_SECRET = os.environ["COINBASE_API_SECRET"]
WEBHOOK_SECRET = os.environ["WEBHOOK_LISTENER_SECRET"]
STORJ_CONTRACT = os.environ["STORJ_CONTRACT_ADDRESS"]
WATCHED_ADDR = os.environ["WATCHED_WALLET_ADDRESS"]
HOST_URL = os.environ["WEBHOOK_HOST"]
CDP_BASE = "https://api.cdp.coinbase.com"
ADV_BASE = "https://api.coinbase.com"
HTTP_TIMEOUT = 10
MAX_RETRIES = 5
processed_tx = set() # prevents duplicate sells (in-memory)
# ───────────────────────── Coinbase Signing ────────────────
def sign_request(method: str, path: str, body: str = ""):
ts = str(int(time.time()))
prehash = ts + method + path + body
sig = base64.b64encode(
hmac.new(API_SECRET.encode(), prehash.encode(), hashlib.sha256).digest()
).decode()
return {
"CB-ACCESS-KEY": API_KEY,
"CB-ACCESS-SIGN": sig,
"CB-ACCESS-TIMESTAMP": ts,
"Content-Type": "application/json"
}
# ───────────────────────── Retry Wrapper ───────────────────
def post_with_retry(url, headers, body) -> Optional[requests.Response]:
for attempt in range(MAX_RETRIES):
try:
r = requests.post(
url,
headers=headers,
data=body,
timeout=HTTP_TIMEOUT,
)
if r.status_code < 500:
return r
log.warning("server error %s — retrying", r.status_code)
except requests.RequestException as e:
log.warning("request failure: %s", e)
time.sleep(2 ** attempt)
log.error("max retries exceeded for %s", url)
return None
# ───────────────────────── Webhook Subscription ────────────
def create_subscription():
path = "/platform/v2/data/webhooks/subscriptions"
body = json.dumps({
"description": "Storj deposit watcher",
"eventTypes": ["onchain.activity.detected"],
"target": {"url": f"{HOST_URL}/webhook", "method": "POST"},
"labels": {
"contract_address": STORJ_CONTRACT,
"event_name": "Transfer"
},
"isEnabled": True
})
headers = sign_request("POST", path, body)
r = post_with_retry(CDP_BASE + path, headers, body)
if not r:
log.error("failed to create webhook subscription")
return
log.info("subscription response %s %s", r.status_code, r.text)
# ───────────────────────── Market Sell ─────────────────────
def place_market_sell():
path = "/api/v3/brokerage/orders"
body_dict = {
"client_order_id": f"sell-storj-{uuid.uuid4()}",
"product_id": "STORJ-USD",
"side": "SELL",
"order_configuration": {"market_market_ioc": {}}
}
body = json.dumps(body_dict)
headers = sign_request("POST", path, body)
r = post_with_retry(ADV_BASE + path, headers, body)
if not r:
log.error("order submission failed (no response)")
return None
try:
payload = r.json()
except Exception:
log.error("non-json order response: %s", r.text)
return None
if r.status_code >= 300:
log.error("order rejected: %s", payload)
return None
log.info("order accepted: %s", payload)
return payload
# ───────────────────────── Webhook Listener ────────────────
app = Flask(__name__)
def verify_sig(sig, payload):
computed = hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, sig)
@app.route("/webhook", methods=["POST"])
def webhook():
payload = request.get_data()
sig = request.headers.get("CB-SIGNATURE-SHA256")
if not sig or not verify_sig(sig, payload):
log.warning("signature verification failed")
abort(400)
try:
evt = request.json
except Exception:
log.warning("invalid json payload")
abort(400)
activity = evt.get("activity") or {}
tx_id = activity.get("transactionHash")
# Idempotency guard
if tx_id and tx_id in processed_tx:
log.info("duplicate event ignored: %s", tx_id)
return {"status": "duplicate"}, 200
if (
activity.get("to") == WATCHED_ADDR and
activity.get("symbol") == "STORJ"
):
log.info("deposit detected: %s", activity)
result = place_market_sell()
if result:
if tx_id:
processed_tx.add(tx_id)
return {"status": "sold"}, 200
log.error("sell failed")
return {"status": "error"}, 500
return {"status": "ignored"}, 200
# ───────────────────────── Startup ─────────────────────────
if __name__ == "__main__":
try:
create_subscription()
except Exception as e:
log.error("subscription creation crashed: %s", e)
log.info("starting webhook listener on :8000")
try:
app.run(host="0.0.0.0", port=8000)
except Exception as e:
log.critical("flask crashed: %s", e)
raise
HTTPS Termination Options
If you already use Cloudflare Tunnel
- Use your Tunnel’s public hostname as
WEBHOOK_HOST
- No local TLS needed
- Ensure Tunnel forwards
/webhook to localhost:8000
Coinbase webhook target → HTTPS Cloudflare → local Flask HTTP.
Optional: Caddy HTTPS Termination
If you do not have Cloudflare Tunnel and need public HTTPS:
- Install Caddy
pkg install caddy
- Caddyfile
Place at /usr/local/etc/caddy/Caddyfile:
storj-autosell.example.com {
reverse_proxy 127.0.0.1:8000
}
- Replace
storj-autosell.example.com with your DNS name.
- Caddy will automatically obtain and renew Let’s Encrypt certificates.
- Start Caddy
service caddy start
- Set:
WEBHOOK_HOST=https://storj-autosell.example.com
before running the Python script.
Steps to Deploy
-
Set up HTTPS (Cloudflare Tunnel or Caddy)
-
Set environment variables
-
Run:
python3 storj_autosell.py
-
Coinbase will deliver webhook events to:
https://<WEBHOOK_HOST>/webhook
-
On deposit, script places a market sell order.
Notes
- You must update
WEBHOOK_LISTENER_SECRET after subscription creation.
- Coinbase API requires trade permissions for order placement.
- Coinbase will deliver only to valid public HTTPS endpoints.