Recycling & Commodity TradingField engagement

The platform takes the trader out of the per-quote loop, so every check they used to make at a glance has to become a rule the system enforces.

Architecture and delivery lead.
The approach, in four words
one state, two viewsobject-level authorisationidempotent binding acceptevery by-eye check becomes a rule
Supplier, on a phonepublic market viewprivate quoteaccept, idempotentTrader workspacethe bookpositionsallocationOne state coreobject-level authorisation on the public and private lineaccept is binding, so it is idempotent by construction
Two audiences reading one state core rather than two applications over one database. The public and private boundary is checked against the object being requested, which is what stops a changed identifier reaching another supplier's private price.
What was at stake

Two audiences on one system. Suppliers see public market information and their own private, executable quotes on a phone. Traders run the internal book. The line between public and private is an authorisation boundary, and accept is binding. A quote reaching a supplier without a person reading it first is the whole point of the product, and it is also the whole risk.

How this works

When a price goes out without a person reading it, the person's judgement has to become rules.

Before a platform like this exists, a trader reads each supplier's request and decides whether to quote. Most of that decision happens in a glance. Is this a grade we buy right now? Does the price still reflect today's market? Is this supplier good for the amount they are describing? Does the request look like what this supplier usually sends? None of these checks is written anywhere, because the trader never needed to write them. They are part of being good at the job.

An executable quote removes the glance. It is a price a supplier can accept on their phone, and acceptance binds the operator immediately, with nobody reviewing it. Every check the trader used to make either exists as a rule the system enforces before the quote is issued, or it does not exist at all. Automation does not make the checks unnecessary. It makes them invisible unless someone deliberately writes them down.

The second idea this kind of system rests on is authorisation. Authentication establishes who someone is. Authorisation decides what they may see and do. Object-level authorisation checks the specific record being requested: this quote belongs to this supplier. The classic failure is a screen that only ever shows a supplier their own quotes, sitting on an interface that will return any quote whose identifier it is sent. Change one number in the request and another supplier's price comes back. The OWASP list of API security risks puts this failure, broken object level authorisation, at the top.

Trader in the loop
Request arrives
read by a person
Checks made at a glance
grade, market, supplier, plausibility
Trader sends a price
judgement applied every time
Executable quote
Request arrives
read by nobody
Every check runs as a rule
or does not run at all
Supplier can bind the operator
one tap, no review
Each glance becomes a rule
The gate does not disappear when the gatekeeper does. It moves into the code.
FigureWhat changes when a quote becomes executable. With a trader in the loop, the checks happen in their head and nobody needs them written down. Without one, each check has to exist as a rule, and anything left unwritten simply stops being checked.
The constraint

Once a quote is executable without a trader looking at it, every check that trader used to make by eye has to exist as an enforced rule, and both surfaces have to share one consistent state. Two applications over one database produce double acceptance, quotes that are live on one screen and stale on the other, and permission bleed across the public and private line. In this shape that bleed is an object-level authorisation failure, which means one supplier reading another supplier's price.

Where it usually breaks

Two applications over one database fail at the seam between them.

Two audiences look like two applications, so teams often build two: a supplier app and an admin panel, each with its own idea of a quote's status, its own caching and its own access checks, sharing a database underneath. Each works on its own. Between them, a quote can be withdrawn in the admin panel and still accepted in the supplier app, because the supplier app read it a moment earlier and never looked again.

The second failure is where the boundary lives. When access is enforced by which screen a user can reach, the interface underneath answers anyone who asks it directly. It is among the most common serious flaws in web interfaces, and it is invisible in normal use because the screens never ask for anything they should not.

01

Live on one side, stale on the other

The trader withdraws or reprices a quote, and the supplier's surface still shows the old one as acceptable.

02

Double acceptance

A binding accept is sent twice by a retry or a second tap, and two trades are booked from one decision.

03

Permission bleed by identifier

Access is checked per screen, so a supplier who edits an identifier in a request receives another supplier's private quote.

04

The missing glance

A check the trader always made, and never mentioned, is not encoded. The first quote that needed it goes out unchecked.

05

The boundary enforced twice

The public and private rule is implemented once in each application, and the two implementations drift apart.

Supplier opens their own quote
the request names it by identifier
Supplier edits the identifier
curiosity is enough, no skill required
Server checks the session only
logged in, so the request is allowed
Another supplier's price returned
the screen never showed it, the interface did
The failure point
A private quote reaches the wrong supplier
a commercial confidence broken without any error
Checking ownership of the object on every request closes this in one place.
Tests were run with deliberately hostile identifiers, because normal use never exercises this path.
A boundary enforced by screens is a boundary that holds only for polite users.
FigureHow one supplier ends up reading another supplier's private price. The screen never offers it. The interface behind the screen checks that the supplier is logged in, and never checks that the quote is theirs. Changing an identifier in the request is enough.
The fork

The reflex, and the fix.

Road not taken

A supplier app and an admin panel over one database

Pull

Two audiences look like two applications, and each one is simpler to build alone.

Why not

Shared state diverges between them, acceptance can fire twice, quotes go stale on one surface while live on the other, and the public and private boundary leaks because it is enforced in two places by two pieces of code.

Road taken

One state core with two views

Accepted

A shared core to design before either surface, and authorisation modelled at the level of each object, not each screen.

Bought

Supplier journeys and the trader book reading one consistent state, acceptance that is idempotent, and a public and private line enforced in exactly one place.

Decision

Enumerate every check the trader made and encode each as a rule, because automating the gatekeeper away does not remove the gate.

How it was built
01Public market view
›
02Private quote
›
03Idempotent accept
›
04Shared state core
›
05Trade record
01

One state core, two views

The supplier journeys and the trader workspace are views over a single core. They are not two applications that happen to share tables, so they cannot disagree about whether a quote is live.

02

Authorisation checked on the object

Every request is checked against the record it asks for, not the screen it came from. That is what stops a supplier reaching another supplier's private quote by changing an identifier in a request.

03

Idempotent acceptance

Accept is binding, so it carries a key. A retry or a double tap on a phone resolves to one execution and never to two trades.

04

Every by-eye check written down as a rule

The checks the trader performed before sending a price were enumerated and encoded. The platform now makes that decision, and it cannot do so by judgement.

05

The uncovered case is flagged

A quote request that no encoded rule covers is held for a person. The platform does not issue an executable price for a situation nobody has described to it.

Inside the key decisions

What each choice cost, and what it bought.

ADesigning the shared core before either surface

Two audiences invite two teams and two applications, and either could have shipped sooner alone. Designing one state core first delayed both surfaces. It meant a quote has one status, read by everyone, and a withdrawal in the trader workspace takes effect on the supplier's phone because there is nothing else for the phone to read.

BEnforcing authorisation on the object, in one place

Per-screen access checks are quicker to write and they look complete in a demo. Checking ownership on every object request, in the core, required modelling who owns what before writing any interface. It made the public and private line a property of the data, so a new screen or a new endpoint inherits it without anyone remembering to add it.

CMaking binding acceptance idempotent

Disabling the accept button after a tap stops most double acceptances from people and none from networks. A key on each acceptance, checked by the core, makes the second request return the first result. It cost a little design on the most important action in the product and removed the possibility of one decision producing two trades.

DInterviewing the trader for their checks

The tempting assumption is that the specification lists the rules. It listed the obvious ones. Sitting with the trader and enumerating what they looked at before sending a price took time and produced rules nobody had thought to specify. Requests no encoded rule covers are held for a person, so a missing rule shows up as a held quote, never as a binding mistake.

Supplier journeys on a phonepublic market view and private quotes
Trader workspacethe internal book
Object-level authorisationwho may see this record, checked on every request
Rules from the trader's checksevery glance, written down and enforced
One state coreone status per quote, idempotent acceptance
Two views, one truth, one boundary.
FigureHow the two audiences sit on one core. Both surfaces are views. The rules and the authorisation boundary live underneath them, once, so neither surface can implement them differently and neither can skip them.
How it was measured

A single headline number hides where a system fails. This work was scored on the dimensions that actually decide whether it holds in production, measured on real, held-out cases rather than the demo path.

Cross-supplier access under adversarial identifiersDouble acceptance under retryState agreement between the two surfacesCoverage of the trader's original checks
The gate
Can a supplier reach a quote that is not theirs by changing an identifier?
Boundary broken, release blocked
the public and private line is not real
Boundary holds at the object
every request checked against ownership
Double acceptance under retry, state agreement between surfaces and coverage of the trader's checks were tested alongside.
FigureThe adversarial test at the centre of the evaluation. No measured figures are published for this engagement. Requests were sent with identifiers belonging to other suppliers, and the only acceptable outcome is a refusal from the core.
figures

No measured figures are published for this engagement. Outcomes are described qualitatively.

What it produces
Without this discipline

Two applications over one database, where a double tap executes twice, a quote is live on one surface and stale on the other, and a changed identifier reaches another supplier's private price.

This system

A single state core with supplier journeys and a trader book as views, idempotent binding acceptance, and object-level authorisation enforcing the public and private boundary in one place.

one state, two viewsobject-level authorisationidempotent binding acceptevery by-eye check becomes a rule
The operating envelope

What it owns, and what it hands to a person.

Handled with confidence
Launch grades and their stated rules
Binding acceptance under retry
Public and private separation at the object level
Flagged for review
A quote case no encoded rule covers
Out of scope by design
Grades and rules that have not been specified
Trader judgement that has not been written down
The honest limit

A clean build proves the loop for the launch grades. Adding grades is rule work, not a rebuild, but it is still work, and the specification's own wording made clear the list would grow. Trader judgement that has never been written down is outside the platform until it is.

What it generalizes to

Any product that automates a human gatekeeper away has the same job list: enumerate every check that person made, turn each into an enforced rule, make the binding action idempotent, and keep every audience on one state. Automated credit decisions, self-service refunds and instant insurance quotes all follow it.

What to take from this

Automating a person out of a decision does not remove the checks they made. It removes them silently unless each one is written down and enforced.

Authentication says who someone is. Authorisation says what they may touch. Check it against the specific record on every request, never against the screen.

Broken object level authorisation tops the OWASP list of API risks because it is invisible in normal use. Test it with hostile identifiers on purpose.

Two audiences do not need two systems of record. Build one state core and give each audience a view of it.

When an automated decision meets a case no rule covers, hold it for a person. A held quote is recoverable and a binding mistake is not.

How we engage

You have a system like this one.
Tell us where it stands.

Whether it is failing, not yet built, or about to meet a scale it has never seen, we can tell you what we see.

Start a conversation
mostafa@opulion.dev · Response within 24 hours · By inquiry