M

How to create a US Youtube automatic posting service with no costs

7 min readView source ↗

Cover image

There is a point where automation stops being a luxury and becomes infrastructure.

For a while, I had already solved part of the problem with TikTok: generate the content, prepare the assets, send the final package to the right destination, and avoid the stupidest possible bottleneck, which is me wasting tons of hours using phones manually to post and create content on every account I have. This alone adds up hundreds of hours in repetitive work that's not optimized. Then it becomes the weak point of the whole system.

You can have a good content pipeline, decent ideas, video generation, metadata, hooks, formats, and publishing rhythm. But if the last step still depends on opening a browser, uploading a file, pasting a title, selecting a privacy option, and pressing publish, then the process is not really automated. It is only automated until the moment it matters.

That was the reason for building a direct YouTube posting service just as I did with TikTok.

Why I Wanted to Remove the Middleman

Until now, one possible path was using a third-party tool like Post Bridge. That works, but it creates a dependency in the most sensitive part of the process: the actual publication. On top of that it costs a subscription that not everyone is happy to pay.

For TikTok, I had already moved toward a more direct model. The local draft CLI checks auth first, stops if the token is expired, and only continues when the publishing path is actually available. That pattern matters because automation should fail before wasting work, not after rendering assets and preparing a post that cannot be delivered.

I wanted the same idea for YouTube.

Not a dashboard. Not a SaaS. Not another abstraction. A small local service that can do one thing well:

Take a video file, metadata, and a few publishing options, then upload it to YouTube through the official API.

That means no Post Bridge in the middle. No Telegram fallback. No manual browser upload. Just my own machine, my own credentials, and YouTube Data API v3.

The Shape of the Service

The first version is deliberately simple.

It is a Node/TypeScript service with two ways to use it:

and a local HTTP endpoint:

The CLI is the important part for automation. A pipeline can render a video, write a JSON file, and call the posting command. The HTTP server is useful if another local service wants to hand off a post without spawning a command.

The post payload contains the essentials:

The important detail is idempotencyKey.

Automation breaks when a retry creates duplicates. If a command fails halfway, or if I run the same job twice, I do not want two identical videos on the channel. So the service writes a local report after each successful upload. If the same idempotencyKey appears again, it returns the previous result instead of uploading again.

That is not glamorous, but it is the difference between a toy script and something I can trust inside a daily publishing workflow.

The OAuth Mess

The hardest part was not uploading the video. The hardest part was the usual Google OAuth ceremony.

The service needs a Google Cloud project, YouTube Data API v3 enabled, and an OAuth client. Once the client ID and secret are configured locally, the service starts a temporary listener on:

Then Google returns an authorization code, the service exchanges it for tokens, and the token is stored locally in:

In theory, that is straightforward.

In practice, Google blocked the first attempt because the app was still in testing mode and the account was not added as a test user. The error was precise: the app had not completed Google verification, and only approved testers could access it.

The fix was not code. It was configuration:

Google Auth Platform → Audience → Test users → add the YouTube account.

After that, the authorization screen showed the actual YouTube permissions:

Manage your YouTube videos

Manage your YouTube account

Once accepted, the token was saved locally and the preflight check passed.

That preflight command is now part of the service:

It verifies that the credentials exist and that a token is stored before any upload happens. This mirrors the lesson from the TikTok workflow: check auth first, stop early, and do not pretend a pipeline is working if the final delivery path is broken.

The First Real Upload

For the test, I used a random vertical MP4 from my local content folder:

The service uploaded it to the YouTube channel SnacksLedger.

The result came back clean:

Then I ran the command again with the same payload.

This time the service returned:

That was the real test. Uploading once proves the API works. Refusing to upload twice proves the automation is safe enough to be reused.

Why Private by Default

The first upload used:

That means the video exists on YouTube, but it is not publicly visible. It does not appear in search, it does not show publicly on the channel, and not everyone with the link can view it.

For testing, this is the right default.

Publishing automation should start conservative. First prove that credentials, upload, metadata, reports, and idempotency work. Then change the privacy setting to public or unlisted when the pipeline is ready.

For future uploads, making a video public is as simple as:

There may still be Google-side limitations if the OAuth app remains unverified, but technically the service already supports the setting.

Why This Matters for Content Distribution

This is not just a technical convenience. It changes the operating model.

Content platforms reward consistency, freshness, and timing. This is obvious on YouTube, but it is even more explicit when you study recommendation systems like the X For You pipeline. Early engagement, post age, dwell, video retention, and timing all matter. The first window after publishing is not decorative. It is the moment where a post either enters the deeper recommendation machinery or dies in the cold start.

That is why manual posting is such a bad bottleneck.

If a video needs to go out at a specific time, the system should publish it at that time. Not when I remember or after I have copied metadata between five tabs.

Automation lets the publishing moment be chosen for the audience, not for my availability.

It also makes the pipeline measurable. Every upload now leaves a local report: video ID, title, privacy status, channel response, raw non-secret metadata. That means future automations can inspect what happened instead of relying on memory or a third-party dashboard.

The Bigger Picture

The service is still small, but it is the correct kind of small.

It does not try to solve content strategy. It only removes one manual step that should never have been manual in the first place.

Generate the video. Prepare the title. Prepare the description. Decide the privacy status. Call the service. Store the report. Move on.

That is the foundation I wanted.

The next natural step is to connect this directly to the existing YouTube Shorts generation pipeline, replacing the Post Bridge delivery step with this local YouTube API service. Once that is done, the whole flow can become:

idea → video package → metadata → direct YouTube upload → report

No browser upload. No third-party posting layer. No manual repetition disguised as work.

That is the point of this kind of automation. Not to make the process look more sophisticated, but to remove the fragile parts until publishing becomes a boring, reliable final step.

Prompts

status: already_uploaded
.secrets/youtube-token.json
"privacyStatus": "private"
npm run setup-check
"privacyStatus": "public"
/Users/your-username/path/to/project/videos/downloaded/4828258-hd_1080_1920_25fps.mp4
POST /posts
npm run post -- --post examples/post.json
{
  "videoPath": "/absolute/path/to/video.mp4",
  "title": "Video title",
  "description": "Video description",
  "tags": ["shorts", "automation"],
  "categoryId": "22",
  "privacyStatus": "private",
  "madeForKids": false,
  "notifySubscribers": false,
  "idempotencyKey": "stable-key-for-this-video"
}
videoId: ngSK-0eoNjk
url: https://www.youtube.com/watch?v=ngSK-0eoNjk
privacyStatus: private
uploadStatus: uploaded
http://localhost:8888/oauth2callback

Related articles