How agent banking float actually works

Float is pre-funded working capital. An agent cannot process a cash-in transaction unless they have float in their wallet to back it. When a customer deposits ₦20,000 with an OPay or Moniepoint agent, the agent's float decreases by ₦20,000 and they hand over cash. The platform reconciles later. Without float, the agent is dead in the water.

The hierarchy works like this: a super-agent is a business or individual that has been onboarded with a larger float pool — sometimes ₦5 million to ₦50 million, funded directly by the platform or by the super-agent's own capital. Under the super-agent sit multiple sub-agents: POS operators, kiosk owners, small retailers. Sub-agents request float from their parent super-agent, the super-agent approves, and the float is transferred between wallets internally within the platform.

This distribution happens via an API call. The sub-agent's app sends a float request. The super-agent receives it in a dashboard or mobile notification, confirms, and the platform moves the float. That approval step — and the API that powers it — is exactly where the vulnerability lives.

How BOLA manifests in the float request endpoint

The float request endpoint typically looks like this:

POST /api/agents/float-request
Authorization: Bearer <sub-agent JWT>

{
  "superAgentId": "SA-100",
  "requestedAmount": 50000,
  "reason": "Low balance"
}

The backend validates the incoming JWT and confirms the caller is an authenticated sub-agent. It checks that SA-100 is a real, active super-agent record in the database. Then it creates the float request and routes it to SA-100's queue.

The missing check: does this sub-agent actually belong to SA-100's hierarchy? The server trusts the client-submitted superAgentId without verifying the organizational relationship. Authentication (who you are) is present. Authorization (what relationships you are permitted to act on) is absent.

This is Broken Object Level Authorization — OWASP API Security Top 10, API1. The object is the super-agent's float pool. The broken authorization is the missing hierarchy membership check.

The exploit, step by step

Super-agent IDs are not secret. They appear in:

Once an attacker sub-agent has a target super-agent ID, the attack is mechanical:

  1. Intercept the float request call from a legitimate session using a proxy (Burp Suite, mitmproxy).
  2. Replace the superAgentId value with the target super-agent's ID.
  3. Submit the request. The platform accepts it, authenticates the sub-agent's JWT, validates the target super-agent exists, and routes the request to the target's queue.
  4. If the platform has auto-approval for requests below a threshold — a common operational shortcut to reduce super-agent workload — the float is disbursed immediately without any human review.
  5. The attacker's wallet is credited. The target super-agent's float pool is debited.

Auto-approval thresholds vary by platform, but ₦50,000 to ₦150,000 per request is common. At those amounts, 20 attacker sub-agents submitting requests just below the threshold drain ₦1–3 million before the anomaly surfaces in any reconciliation report.

Variant: manipulating the approval endpoint

Some platforms extend limited approval rights to senior sub-agents or network coordinators — effectively peer-to-peer float approvals within a cluster. The approval endpoint might look like:

POST /api/agents/float-request/approve
Authorization: Bearer <approver JWT>

{
  "requestId": "FLR-8821",
  "approverId": "AGT-200",
  "approved": true
}

If the server checks that the caller is an authenticated agent with approval rights but does not verify that approverId in the body matches req.user.agentId from the JWT, an attacker can substitute any approver's ID. They approve their own float request by impersonating a legitimate approver — the float moves, and the audit log shows a different agent authorizing it.

The fix for this variant is identical in structure: the approverId in the request body must be ignored entirely. Use req.user.agentId extracted from the verified JWT as the approver identity. Never trust the client to self-report which identity is performing the action.

Financial impact in Naira terms

Float is not a ledger abstraction. It is real Naira, either funded by the super-agent's own capital or fronted by the platform against a credit facility. A super-agent holding ₦5 million in float and a pool of 200 sub-agents has a meaningful attack surface.

The fix: server-side hierarchy validation

The core fix is a single database check that runs before any float request is created or queued. The server must confirm that the requesting agent is a direct (or permitted-depth) sub-agent of the specified super-agent.

// On receipt of POST /api/agents/float-request
// req.user.agentId is extracted from the verified JWT — never from the request body

const relationship = await db('agent_hierarchy')
  .where({
    sub_agent_id: req.user.agentId,
    super_agent_id: body.superAgentId,
    status: 'active',
  })
  .first();

if (!relationship) {
  return res.status(403).json({
    error: 'Not authorized to request float from this super-agent',
  });
}

// Only now proceed to create the float request
const floatRequest = await createFloatRequest({
  subAgentId: req.user.agentId,
  superAgentId: body.superAgentId,
  amount: body.requestedAmount,
});

The agent_hierarchy table stores the explicit parent/child relationship for every agent pair. This lookup is indexed on both sub_agent_id and super_agent_id, making it a constant-time check regardless of network size.

Additional controls required alongside the lookup

Example finding

Float hijack via unvalidated superAgentId — agency banking platform

During a penetration test on an agency banking platform, we operated two test accounts: a sub-agent under super-agent SA-123 and an observer account with no hierarchy relationship to SA-456. Using our sub-agent session, we modified the superAgentId field in a float request from SA-123 to SA-456. The platform accepted the request, inserted it into SA-456's approval queue, and triggered an auto-approval rule for amounts below ₦100,000. We submitted ₦90,000. Within 40 seconds, our test sub-agent's wallet was credited ₦90,000 sourced from SA-456's float pool — a super-agent we had no contractual or platform relationship with. Fix priority: critical. We also confirmed the approval endpoint variant: substituting a legitimate approver's agent ID in the body bypassed the approval identity check entirely.

Why automated scanners miss this

Automated API scanners test the shape and schema of responses. They confirm that superAgentId is a valid UUID, that the endpoint returns 200 on a well-formed request, and that authentication headers are required. They do not model business relationships. A scanner has no concept of which sub-agent is permitted to draw float from which super-agent — that relationship exists only in your database and in your business rules.

Catching this requires a tester operating two authenticated accounts simultaneously — one with a known hierarchy relationship and one without — and manually tracing the float request workflow across both roles. That is a multi-step, role-aware test that only manual review can perform.

If your float request endpoint does not check the agent hierarchy server-side before queuing a request, you have a critical vulnerability in production right now.

Get a security review

Frequently asked questions

Is this the same as IDOR?

Yes. BOLA is the OWASP API Security Top 10 name for what was previously called IDOR (Insecure Direct Object Reference). The difference is scale — in agent banking, the 'objects' being referenced are float accounts with real Naira value.

How do we structure the agent hierarchy in the database to make this easier to validate?

Use a closure table or adjacency list in your database that stores every super-agent/sub-agent relationship explicitly. Validation then becomes a single indexed lookup rather than a recursive query.

Can super-agents see this attack happening?

Only if you show super-agents their incoming float request queue with agent identity details. Many platforms show only the amount requested, not the requesting agent's full profile. Super-agents cannot detect anomalous requests they are not equipped to recognize.

Related reading

Blog: BOLA in payment APIs — the pattern explained

Industries: Mobile money security testing

Services: API security testing · Penetration testing

Guides: Fintech security checklist