Icrc1_transfer from canister always returns InsufficientFunds: ledger_balance=0 despite correct balance — both ICP and ckUSDC ledgers

Hi everyone,

I’m building an autonomous trading bot canister on ICP with Caffeine.AI and have been stuck on a persistent InsufficientFunds: ledger_balance=0 error when calling icrc1_transfer from within the canister. I’ve exhausted all code-level fixes and would appreciate community insight.

Canister ID: pbj3p-byaaa-aaaas-qadna-cai

The Problem: icrc1_balance_of({ owner = pbj3p-..., subaccount = null }) always returns the correct balance (2,250,000 for ckUSDC, 100,000,000 for ICP). But any icrc1_transfer call from the canister fails immediately with InsufficientFunds: ledger_balance=0.

What I’ve ruled out (with proof):

  • :cross_mark: Wrong subaccount — SHA224-computed Account ID matches IC Explorer To-address exactly

  • :cross_mark: Caller-derived subaccount problem — tested with Timer.setTimer, caller_context=NONE(timer) confirmed

  • :cross_mark: Cycles starvation — cycles=1,116,788,978,000 (~1.1T), more than sufficient

  • :cross_mark: Canister frozen — icrc1_name() query call to the ledger succeeds fine

  • :cross_mark: ckUSDC-specific issue — same error on the ICP ledger (ryjl3-...)

  • :cross_mark: amount < fee — tested with 50,000 base units (5x the fee)

  • :cross_mark: Pending/notify state — account_balance (legacy) and icrc1_balance_of both return identical values

  • :cross_mark: Pre-check await context switch — tested with transfer as the very first and only await

  • :cross_mark: Wrong Actor definition — using standard Motoko ICRC-1 actor interface

Timer test result (key evidence):

Timer fired | BACKEND_PRINCIPAL=pbj3p-byaaa-aaaas-qadna-cai
caller_context=NONE(timer)
balance_before=100000000
TRANSFER FAILED | error=InsufficientFunds: ledger_balance=0
amount=50000 | fee=10000

Minimal Motoko code that fails:

let ledger = actor("ryjl3-tyaaa-aaaaa-aaaba-cai") : actor {
  icrc1_transfer : (TransferArg) -> async TransferResult;
};
let result = await ledger.icrc1_transfer({
  from_subaccount = null;
  to = { owner = ownerPrincipal; subaccount = null };
  amount = 50000;
  fee = null;
  memo = null;
  created_at_time = null;
});
// Returns: Err(#InsufficientFunds { balance = 0 })

The canister has 1 ICP on { owner = pbj3p-..., subaccount = null } confirmed by both legacy and ICRC-1 balance APIs. The transfer fails regardless of context (public shared func, timer callback, with or without pre-check).

Has anyone seen this before? Is there something about how a canister must be initialized or configured before it can send outgoing ledger transfers?

Any help appreciated.

If you’re using Caffeine, could it be that the canister ID pbj3p-byaaa-aaaas-qadna-cai is the frontend canister, but it is the backend canister (that may not have any funds) that is making the call to the ledger?

It is an asset canister.

Hello Mathias,

I’m pretty sure that pbj3p-byaaa-aaaas-qadna-caiis the backend canister, since I’ve been tinkering with it for a long time. I’ve currently used up 150 credits to move the transfer from the frontend to the backend so that something like an autonomous bot can work at all. Unfortunately, Caffeine V3 has now also reached its limit. That’s why I’m posting here.

I forwarded your question to Caffeine again, and received the following response:

The canister ID pbj3p-byaaa-aaaas-qadna-cai is the backend canister, not the frontend. Here is the evidence:

  • debugSelfPrincipal() returns pbj3p-byaaa-aaaas-qadna-cai from within the Motoko code — frontend canisters don’t expose Motoko functions like this

  • icrc1_balance_of({ owner = pbj3p-..., subaccount = null }) returns 2,250,000 base units of ckUSDC and 100,000,000 e8s of ICP — funds that were explicitly sent to this principal

  • IC Explorer confirms incoming transfers to pbj3p-byaaa-aaaas-qadna-cai with Source Canister = ryjl3-tyaaa-aaaaa-aaaba-cai (the ICP ledger) and xevnm-gaaaa-aaaar-qafnq-cai (the ckUSDC ledger)

  • The canister exposes backend functions like executeBuyICP, debugTimerCallerTest, icrc1_balance_of queries — these are all Motoko backend methods, not frontend asset serving

So the hypothesis — that pbj3p-... is the frontend canister while a separate, unfunded backend is making ledger calls — does not match the evidence. The funds are confirmed on pbj3p-..., and all Motoko logic (including the failing icrc1_transfer calls) runs on that same canister.

However, the question does point to something worth double-checking: it would be useful to explicitly confirm the frontend canister ID and verify that no logic accidentally runs there. On Caffeine, the frontend canister only serves static assets and should never be making inter-canister calls to ledgers.

The core issue remains: icrc1_transfer from pbj3p-... (the confirmed backend canister, with confirmed funds, confirmed cycles, confirmed timer context without user-caller) still returns InsufficientFunds: ledger_balance=0. That is the open and unresolved question.

Its a crazy world huh

Helmet guy provided a proof the other answers were correct

My guess - user thinks: I’ve moved the transfer call from the frontend (js) to the backend (motoko). Takes the first id they can find - it’s the asset canister id.

But the asset canister hosts your frontend, it’s a static file server. It’s also ‘backend’, so you have two backend canisters, one serves files, the other is your custom motoko api.

Yes, that’s my take on the whole thing as well. I’ve often asked myself why the system knows so little about fundamental things and has to do so much research just to try out all sorts of things afterward.

Well, it was definitely an interesting experiment for me. It could have worked. I would have liked to have had a small prototype to get a good kickstart into ICP development and then study the code.

But I think I’ll stick with my game in Unity DOTS and C# with Rider. That three-day of vibe coding with Caffeine was probably just procrastination anyway, putting off my game, which I’ve been working on for a few years now. The whole vibe coding thing isn’t for me anyway—it’s very tiring and kills any joy in development.

Well, Caffeine confidently and convincingly assured me that this is how it’s supposed to work—a wallet in the canister that allows the canister to perform swaps on ICPSwap autonomously. That makes sense to me. Or does it not?
And believe me, I definitely didn’t just grab the first ID that came to mind; I’ve now iterated on the problem 94 times with Caffeine. And now Caffeine itself has reached a dead end and assumes there’s a fundamental ledger issue or broken canister state, so it suggested “escalating” it here in the forum to get some help - which I did ;D

Well, screw it—it’s just playing around anyway.

its not your fault its the cofee

Do you have github repo at caffeinepub. We could try to get it to work somehow.
When i tested caffeine and built mixer, i had to get canister backend to be actor that can hold and transfer funds when called, most likely would work with timers as well. Lets say you get this working, next step is to get it working with ICPSwap, what i did try (just swaps, but failed).

I think for this to be built, CaffeineAI team has to teach AI to implement them correctly.

T shirt worthy. Make this and make money

make website no shirt