projects
Jasper
Jasper imports your Instagram photos to Grain or Spark while preserving original timestamps. Your memories appear with their original dates, not the import date.
The name follows ATProto import tools using mineral names — a nod to the pattern established by Malachite. Jasper is a red-orange quartz, fitting for something that preserves photographic memories.
What it does
- Preserves timestamps — Photos appear with their original Instagram dates
- Multiple targets — Import to Grain or Spark
- Stories and videos — Instagram stories and videos import to Spark (Grain doesn't support them)
- Handles all export formats — Works with 2022, 2023, 2024, and 2025 Instagram exports
- Gallery-based (Grain) — Photos are organised into Grain galleries you choose or create
- Multi-image posts (Spark) — Carousel posts import as a single Spark post with up to 12 images
- Skips duplicates — Already-imported posts are detected and skipped
- Dry run mode — Preview what would be imported before committing
- OAuth authentication — Secure login via your existing AT Protocol identity
- Web interface — Import from your browser at jasper.croft.click
Installation
# Install globally with pnpm
pnpm install -g @ewanc26/jasper
# Or use directly with npx
npx @ewanc26/jasper -i instagram-export.zip --dry-run
Usage
Interactive Mode
Run without arguments for guided prompts:
jasper
Jasper will prompt you to select a target platform (Grain or Spark) and, for Grain, select or create a gallery before importing.
Command Line
# Import from ZIP to Grain (default)
jasper -i instagram-export.zip
# Import from ZIP to Spark
jasper -i instagram-export.zip --target spark
# Import from extracted directory
jasper -i instagram-export/
# Preview without posting (dry run)
jasper -i instagram-export.zip --dry-run
# Limit to first 50 posts
jasper -i instagram-export.zip --limit 50
# Override alt text for all photos
jasper -i instagram-export.zip --alt "Instagram photo"
# Skip confirmation prompts
jasper -i instagram-export.zip -y
# More verbose output
jasper -i instagram-export.zip -v
Authentication
OAuth (Recommended)
# Sign in via browser
jasper --oauth-login
# Sessions are stored in ~/.jasper/oauth.json
OAuth uses a loopback callback server on port 8766. Your browser opens to your PDS's authorisation screen, you approve, and the session is saved automatically. Subsequent imports use the stored session — no re-authentication needed.
App Password
If OAuth isn't available, you can use an app password:
jasper -i export.zip --handle your.handle --password your-app-password
Generate an app password at bsky.app/settings/app-passwords.
Getting Your Instagram Export
- Open Instagram (app or web browser)
- Go to your profile
- Tap the menu (☰) → Settings → Accounts Centre
- Select "Your information and permissions"
- Choose "Export your information"
- Select "Export to device"
- Choose what to include (or select "All information")
- Select JSON format
- Choose media quality (High recommended)
- Enter your password to confirm
- Wait for the email notification (can take hours to days)
- Download the ZIP file when ready
Jasper will locate posts_1.json and stories_1.json automatically, handling all export format variations.
Target Platforms
Jasper supports two AT Protocol platforms for importing. Each has different capabilities:
| Feature | Grain | Spark |
|---|---|---|
| Photos | ✅ | ✅ |
| Videos | ❌ | ✅ |
| Stories | ❌ | ✅ |
| Galleries | ✅ | ❌ |
| Multi-image posts | ❌ (one photo per record) | ✅ (up to 12 images) |
| Alt text | Optional | Required |
| Max image size | 1 MB | 5 MB |
Choose the target that fits your content. Use --target grain or --target spark (default: grain).
Data Model
Grain
Jasper creates three types of records:
social.grain.photo— The image blob with aspect ratio and timestampsocial.grain.gallery— A container you create or select for organising photossocial.grain.gallery.item— Links each photo to your chosen gallery with position
Photos must be linked to a gallery to display properly on grain.social. Each record holds one photo.
Spark
Jasper creates two types of records:
so.sprk.feed.post— A post with a media union of images or videoso.sprk.story.post— A story with a media union of images or video
Spark uses a media union — each post has either so.sprk.media.images (up to 12 images) or so.sprk.media.video. Carousel Instagram posts become a single Spark post with multiple images. Videos are uploaded as blobs and attached with so.sprk.media.video.
What Gets Imported
Imported to Grain:
- ✅ Photos (JPEG, PNG, WebP, GIF)
- ✅ Original timestamps
- ✅ Captions (as alt text)
- ✅ Carousel posts (each photo as a separate record)
Imported to Spark:
- ✅ Photos (JPEG, PNG, WebP, GIF)
- ✅ Videos (MP4, MOV)
- ✅ Original timestamps
- ✅ Captions (as alt text)
- ✅ Carousel posts (up to 12 images per post)
- ✅ Stories (image and video)
Not imported:
- ❌ Reels
- ❌ Stories to Grain (not supported)
- ❌ Videos to Grain (not supported)
Options
| Option | Description |
|---|---|
-i, --input <path> |
Path to Instagram export ZIP or directory |
--target <platform> |
Target platform: grain or spark (default: grain) |
--dry-run |
Preview posts without importing |
--limit <N> |
Import at most N posts |
--reverse |
Process newest posts first (default: oldest first) |
--alt <text> |
Override alt text for all photos (default: captions) |
-v, --verbose |
Enable debug logging |
-q, --quiet |
Suppress non-essential output |
-y, --yes |
Skip confirmation prompts |
--oauth-login |
Sign in via OAuth |
--logout [DID] |
Sign out (removes stored session) |
--list-sessions |
List stored OAuth sessions |
Daily Limits for Large Exports
Large Instagram exports (hundreds of photos) should be split across multiple days to avoid hitting PDS blob upload quotas. Jasper supports resumable imports with daily limits:
# Import with daily limit (default: 100 posts/day)
jasper -i large-export.zip --daily-limit 50
# Resume previous import session
jasper --resume
# List pending import sessions
jasper --list-imports
# Clear all saved import state
jasper --clear-imports
| Option | Description |
|---|---|
--daily-limit <N> |
Maximum posts to import per day (default: 100) |
--resume |
Resume previous import session |
--list-imports |
List pending import sessions |
--clear-imports |
Clear all saved import state |
When the daily limit is reached, Jasper saves your progress and prompts you to continue the next day. Run jasper --resume to continue importing.
Web Interface
Jasper has a browser-based interface at jasper.croft.click — no installation required. The web app runs entirely in your browser: your Instagram export is parsed locally, and photos are uploaded directly to your PDS.
The web app uses @atproto/oauth-client-browser for authentication. No data passes through any intermediate server.
Source: packages/jasper-web in the monorepo.
Data Storage
All data stays on your machine:
| Location | Content |
|---|---|
~/.jasper/oauth.json |
OAuth session tokens |
~/.jasper/imports/ |
Import state for resumable sessions |
~/.jasper/logs/ |
Debug log files |
No data is sent to any server except your chosen Grain or Spark account.
OAuth Scope
Jasper requests minimal permissions based on target platform:
Grain:
atproto blob:*/* repo:social.grain.photo repo:social.grain.gallery repo:social.grain.gallery.item
Spark:
atproto blob:*/* repo:so.sprk.feed.post repo:so.sprk.story.post
This follows ATProto's granular permission model — no broad transition:generic scope.
Development
Build from source:
git clone https://github.com/ewanc26/pkgs.git
cd pkgs
pnpm install
pnpm --filter @ewanc26/jasper build
Run in dev mode:
pnpm --filter @ewanc26/jasper dev
Build the web app:
pnpm --filter @ewanc26/jasper-web build
Requirements
License
AGPL-3.0-only.
Contact
Ewan Croft — contact@ewancroft.uk
← all docs