@volter/twin-instagram
v0.1.37
Published
Local Instagram twin (a professional account's Reels: resumable containers, the rupload upload, status_code, media_publish) with an instagram.com profile mirror, built on @volter/world-core.
Readme
@volter/twin-instagram — a professional account's Reels on Instagram
An Instagram professional account and its Reels: the Graph API's content publishing flow (a
resumable Reels container, the upload to rupload.facebook.com, the container's status_code,
media_publish), the account and media nodes, the publishing limit, and an instagram.com-style
profile to preview each Reel as instagram.com shows it before a deploy sends it — offline, against
local state.
bun packages/twin/instagram/src/cli.ts serve --root /tmp/world # the Graph API and the upload host
bun packages/twin/instagram/src/cli.ts mirror --root /tmp/world # the profile, on the same statePaths take an optional Graph version (the changelog's table, judged at the World's instant: /v21.0/
… /v26.0/ on 2026-09-27; an expired one is served as the oldest usable, as Meta defaults it). A
token rides Authorization: Bearer|OAuth, or access_token, and needs every permission of its
reference's Requirements row (a container: instagram_basic + instagram_content_publish +
pages_read_engagement; a publish: instagram_basic + instagram_content_publish; or the Instagram
Login instagram_business_* pair — except that a resumable upload session and its rupload POSTs are
"Only for apps that have implemented Facebook Login for Business").
Coverage
| Area | What the twin serves |
|---|---|
| GET /{ig-user-id} · GET /me | The account's fields as asked (id alone by default): username, name, biography, website, followers_count, follows_count, media_count (the live media the twin holds), profile_picture_url, has_profile_pic. A field the node lacks is (#100) Tried accessing nonexisting field; a documented one the twin does not model is refused by name. Only the token's own account is readable. |
| POST /{ig-user-id}/media | A Reels container for a resumable upload: media_type=REELS, upload_type=resumable, caption (2200 characters, 30 hashtags, 20 @ tags), share_to_feed, cover_url, thumb_offset, audio_name, is_ai_generated — JSON or form-encoded. Answers {id, uri}, the uri on the upload host (under the twin's own base when served). 400 containers per rolling 24 hours. Images, carousels, stories, VIDEO and video_url uploads, collaborators, tags, locations, trial params and product tags are refused by name as 422 [twin gap]. |
| POST rupload.facebook.com/ig-api-upload/{version}/{container} | The bytes, with the same token as the Graph API (Authorization: OAuth <token>, the host's documented scheme; Bearer and access_token also work — no page says the host refuses them), offset and file_size: one POST, or pieces, each POST's offset the first byte it carries. The completing POST answers the documented {"success":true,"message":"Upload successful."}; a failure the documented debug_info envelope (unauthorized user request for a missing, unknown or under-permitted token). The twin's own shapes, where the docs give none: a piece answers {offset, file_size} (no success), a GET of the URL answers file_offset as the Graph Resumable Upload guide does for graph.facebook.com (instagram.upload.offset_discovery), and a declared file_size over 300 MB is refused before any byte is staged. file_url uploads are refused by name. |
| GET /{container-id}?fields=status_code,status | IN_PROGRESS until five seconds of world time after the last byte, then FINISHED — or ERROR with the subcode on status when the file is outside the Reels specification (MOV/MP4 with the moov atom first, H.264 or HEVC, 23-60 FPS, AAC up to 48 kHz and 2 channels, at most 1920 columns, aspect 0.01:1 to 10:1, 3 s to 15 min, 300 MB, an average bitrate within 25 Mbps video + 128 kbps audio: 2207026; a thumb_offset past the end, unless a cover_url is set, which Meta uses instead: 2207057; edit lists, GOP structure and chroma subsampling are not read — instagram.containers.edit_lists); PUBLISHED once published; EXPIRED 24 hours after creation. A status is a fold of stored instants — a frozen World clock keeps a Reel IN_PROGRESS: advance it (volter world clock advance 5s). |
| POST /{ig-user-id}/media_publish | creation_id → {id} of the new media. Not ready is 9007/2207027, a failed container -1/2207032, an expired one -2/2207020, an unknown, foreign or already-published one 24/2207008; 50 API-published posts in a moving 24 hours (9/2207042 after) — counted from the media published through this twin, a deleted one included, a Reel pulled by a refresh not (it may have been posted in the app). |
| GET /{ig-user-id}/media · GET /{ig-media-id} · DELETE /{ig-media-id} | The media list newest first (limit, after cursors and a next link), the media node's fields (media_type VIDEO, media_product_type REELS, caption, timestamp, shortcode, the /reel/ permalink, media_url, thumbnail_url, owner, username, is_shared_to_feed, counts), and a delete (instagram_manage_contents). |
| bytes | media_url serves the uploaded Reel under the twin's base at the CDN's path shape through the kernel's ranged reads: HTTP Range 206/416, an open-ended range capped at 8 MiB, a whole file streamed. A Reel with a cover_url answers it as thumbnail_url (the twin fetches nothing); one without answers a stand-in poster, since the twin decodes no frames. A seeded profile picture is served the same way. |
| GET /{ig-user-id}/content_publishing_limit | {data:[{quota_usage}]} (the same API-published count), config {quota_total: 50, quota_duration: 86400} when asked — or for the reference example's rate_limit_settings — and since no older than 24 hours. |
| errors | Meta's envelope {error:{message, type, code, error_subcode, is_transient, error_user_title, error_user_msg, fbtrace_id}} with the Error Codes reference's codes; an unknown node is Unsupported get request (100/33); 405 under --read-only. |
| connector | Perform: a Reel crosses as the container (kept from the moment it opens), the bytes to the answered rupload uri in 8 MiB chunks (the kernel gives a request 30 s) with the confirmed offset kept after each — a credentialed absolute URL on the host the descriptor declares (plain https, no port), or, when a twin stands as the vendor, the anchored /ig-api-upload/… path under the root — then status_code at 0, 60 and 120 s (Meta: once a minute; hard cap 120 s, then a retryable failure that keeps the container), then media_publish and the permalink for the receipt. A retry resumes the upload at the kept offset (a kept target is re-checked against what the vendor could have answered) or publishes the kept container; a resumed POST the host refuses with 400 is checked against the container's status — FINISHED (or ERROR / PUBLISHED): carry on; IN_PROGRESS (Meta may be processing the whole file): wait on it, and if the wait ends IN_PROGRESS the next retry opens a new container; EXPIRED: a new container next retry (a 400 for another cause, such as a bad credential, costs one container per two retries); a 429 keeps it. An ERROR naming the file (2207026, 2207057) refuses the entry; any other ERROR fails retryably with a fresh container next time. A container that reads PUBLISHED though its answer was lost lands the one Reel with this caption (every @ ignored, as Meta strips them) since the container opened on the vendor's clock (its 24-hour lifetime is counted on the local one); a caption-less Reel or no single match refuses the entry, and an unreadable media list fails retryably — never a second publish, never a blocked queue. A delete crosses as DELETE. Refresh: /me and the media list. Both charge the pack's rate budget, one ledger per sealed credential. |
| mirror | instagram.com over the twin's own Graph API: log in with the username and the twin-issued token in place of a password; the profile (avatar, username, the counts it holds, name, bio, website), the Reels grid (9:16 tiles with a play glyph, each the Reel's own first frame or its cover), and a full-height Reel viewer (playing, muted, looped) with the caption and username. |
| /_twin/* | Twin-only control plane: seed an account (id, username, profile fields, a base64 profile picture) and an access token with its permissions — held only as its SHA-256, never in the clear. |
src/instagram-capabilities.ts is the denominator: 116 capabilities, 71 done. What is not
modelled is enumerated there as a todo — image posts, carousels, stories, hosted video_url uploads,
tags and collaborators, comments, insights, hashtag search, business discovery, webhooks, the
Instagram Login token flow, and the composer on the mirror — not omitted.
The sealed credential. The Graph API and the upload host take the same token, and both take the
OAuth scheme (the Graph API's Resumable Upload guide sends Authorization: OAuth to
graph.facebook.com), so seal it as {"headers":{"authorization":"OAuth <token>"}}. A bare token
seals as Bearer: a twin standing as the vendor takes it on its upload route, but Meta documents only
OAuth for rupload.facebook.com.
The rate budget (src/instagram-budget.ts): 50 publishes and 400 containers per 24 hours, spread
over the kernel's one-hour window and rounded down; a burst that admits one perform of a 300 MB Reel.
Routing the credential. The kernel sends the sealed credential to an absolute URL on any exact
host rule of this descriptor, whatever its port — so graph.instagram.com and
scontent.cdninstagram.com are eligible as well as rupload.facebook.com. The connector sends an absolute
URL only to rupload.facebook.com over plain https.
