diff --git a/make_post_sell/lib/torrent.py b/make_post_sell/lib/torrent.py
new file mode 100644
index 0000000..927b180
--- /dev/null
+++ b/make_post_sell/lib/torrent.py
@@ -0,0 +1,153 @@
+"""torrent.py — automatic .torrent + magnet link generation for product files.
+
+When a shop has torrent_enabled, this runs after every product file upload:
+ 1. Stream file from S3 to a temp file
+ 2. Build a .torrent using torf (pure Python, no C deps)
+ 3. Upload the .torrent back to S3 at {s3_path}/product.torrent
+ 4. Derive the magnet link from the torrent info hash
+ 5. Save the magnet link to product.torrent_magnet_link in the DB
+
+Fire-and-forget daemon thread — same pattern as s3_mirror.
+"""
+
+import hashlib
+import logging
+import tempfile
+import threading
+import os
+
+import boto3
+
+log = logging.getLogger(__name__)
+
+# Public trackers embedded in every torrent
+DEFAULT_TRACKERS = [
+ "udp://tracker.opentrackr.org:1337/announce",
+ "udp://open.stealth.si:80/announce",
+ "udp://tracker.torrent.eu.org:451/announce",
+ "udp://tracker.openbittorrent.com:80/announce",
+]
+
+
+def _capture_s3_creds(client):
+ """Extract serialisable credentials from a boto3 client."""
+ creds = client._request_signer._credentials
+ meta = client.meta
+ return {
+ "endpoint": meta.endpoint_url,
+ "region": meta.region_name,
+ "access_key": creds.access_key,
+ "secret_key": creds.secret_key,
+ }
+
+
+def _make_client(creds):
+ return boto3.session.Session().client(
+ "s3",
+ region_name=creds["region"],
+ endpoint_url=creds["endpoint"],
+ aws_access_key_id=creds["access_key"],
+ aws_secret_access_key=creds["secret_key"],
+ )
+
+
+def _build_torrent(file_path, name, trackers):
+ """Create a torf.Torrent from a local file and return (torrent, magnet_uri)."""
+ try:
+ import torf
+ except ImportError:
+ raise RuntimeError(
+ "torf is required for torrent generation — pip install torf"
+ )
+
+ t = torf.Torrent(
+ path=file_path,
+ name=name,
+ trackers=[[tr] for tr in trackers],
+ private=False,
+ comment="permacomputer.com — CWE-407 patched OS image",
+ source="permacomputer",
+ )
+ t.generate()
+ magnet = str(t.magnet())
+ return t, magnet
+
+
+def generate_torrent(s3_client, bucket, s3_key, s3_path, product_id,
+ session_factory, trackers=None):
+ """Download file from S3, generate .torrent, upload, save magnet link.
+
+ Designed to run in a background thread — never call directly from a request.
+ """
+ if trackers is None:
+ trackers = DEFAULT_TRACKERS
+
+ name = os.path.basename(s3_key)
+
+ log.info("torrent: generating for product=%s key=%s", product_id, s3_key)
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ local_path = os.path.join(tmpdir, name)
+ torrent_path = os.path.join(tmpdir, f"{name}.torrent")
+
+ # 1. Stream file from S3
+ log.info("torrent: downloading s3://%s/%s", bucket, s3_key)
+ s3_client.download_file(bucket, s3_key, local_path)
+
+ # 2. Build .torrent + extract magnet link
+ log.info("torrent: building torrent for %s", local_path)
+ torrent_obj, magnet_uri = _build_torrent(local_path, name, trackers)
+
+ # 3. Write .torrent file
+ torrent_obj.write(torrent_path)
+ log.info("torrent: magnet=%s", magnet_uri)
+
+ # 4. Upload .torrent to S3 alongside the product file
+ torrent_s3_key = f"{s3_path}/product.torrent"
+ s3_client.upload_file(
+ torrent_path,
+ bucket,
+ torrent_s3_key,
+ ExtraArgs={
+ "ACL": "public-read",
+ "ContentType": "application/x-bittorrent",
+ "ContentDisposition": f'attachment; filename="{name}.torrent"',
+ "CacheControl": "public, max-age=86400",
+ },
+ )
+ log.info("torrent: uploaded .torrent to s3://%s/%s", bucket, torrent_s3_key)
+
+ # 5. Save magnet link to DB
+ try:
+ with session_factory() as session:
+ from ..models.product import Product
+ product = session.get(Product, product_id)
+ if product is not None:
+ product.torrent_magnet_link = magnet_uri
+ session.add(product)
+ session.commit()
+ log.info("torrent: saved magnet link for product=%s", product_id)
+ except Exception:
+ log.exception("torrent: failed to save magnet link for product=%s", product_id)
+
+
+def generate_torrent_async(s3_client, bucket, s3_key, s3_path, product_id,
+ session_factory, trackers=None):
+ """Fire-and-forget: generate torrent in a daemon thread."""
+ creds = _capture_s3_creds(s3_client)
+
+ def _run():
+ try:
+ fresh_client = _make_client(creds)
+ generate_torrent(
+ fresh_client, bucket, s3_key, s3_path, product_id,
+ session_factory, trackers=trackers,
+ )
+ except Exception:
+ log.exception(
+ "torrent: background generation failed for product=%s", product_id
+ )
+
+ t = threading.Thread(target=_run, daemon=True, name=f"torrent-{product_id}")
+ t.start()
+ log.info("torrent: background thread started for product=%s", product_id)
diff --git a/make_post_sell/templates/product_edit.j2 b/make_post_sell/templates/product_edit.j2
index 71f4bb3..a00846c 100644
--- a/make_post_sell/templates/product_edit.j2
+++ b/make_post_sell/templates/product_edit.j2
@@ -289,13 +289,27 @@ Your cover (thumbnail1) will show up on search pages.
{% if torrent_enabled %}
-
-
+
+ {% if torrent_magnet_link %}
+
+ Upload the product file to generate the magnet link automatically. +
+ {% endif %} -