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:
- Referral codes printed on agent onboarding flyers (
Ref: SA-4482) - QR codes agents display at their POS terminals, which encode the super-agent's network ID
- Agent network directories some platforms publish for merchant discovery
- Error messages and API responses that leak the parent super-agent ID when a sub-agent loads their own profile
Once an attacker sub-agent has a target super-agent ID, the attack is mechanical:
- Intercept the float request call from a legitimate session using a proxy (Burp Suite, mitmproxy).
- Replace the
superAgentIdvalue with the target super-agent's ID. - 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.
- 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.
- 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.
- Direct float theft: 20 unrelated sub-agents each submit a ₦200,000 auto-approved request → ₦4 million drained before the next daily reconciliation.
- Reversal complexity: Float was disbursed. Transactions were completed against it. Reversals require the platform's operations team to claw back from agent wallets, which may already be spent. Legal action is the realistic recovery path.
- Super-agent liability: Depending on the platform contract, the super-agent may bear partial liability for float shortfalls in their network — making them both victim and liable party simultaneously.
- CBN reporting: Unauthorized fund transfers above ₦5 million trigger mandatory incident reporting to the CBN under the Risk-Based Cybersecurity Framework. Platforms that delay reporting compound their regulatory exposure.
- NDPA exposure: If the attack surface leaks agent profile data (names, BVN-linked records) as part of the hierarchy enumeration, that is a data breach under the Nigeria Data Protection Act with fines up to 2% of annual gross revenue.
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
- Eliminate auto-approval above ₦50,000. Every float request above this threshold must require an explicit, separately authenticated action from the super-agent — not a background job or timeout-based approval.
- Lock the approver identity server-side. In approval endpoints, derive the
approver's identity exclusively from
req.user(the verified JWT payload). Discard anyapproverIdfield submitted in the request body. - Alert on hierarchy mismatches. Log every float request attempt. When the hierarchy check fails, emit an alert to your security monitoring pipeline — not just a 403. A pattern of failed hierarchy checks from one agent ID indicates active probing.
- Expose the requesting agent's identity to the super-agent. Super-agents should see the full agent profile (name, agent code, onboarding date, transaction history) alongside each float request — not just the amount. This makes social-engineering-style requests harder to disguise.
- Rate-limit float requests per sub-agent per day. Even with the hierarchy
check in place, a sub-agent should not be able to submit more than a configurable number of
float requests in a 24-hour window. Implement this with a counter in Redis keyed by
agentIdwith a 24-hour TTL.
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 reviewFrequently 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