Never accept the first offer

How we built Cerebro and won the Causa Prima x Claude hackathon, mistakes included.

Group photo of the hackathon participants
54 people, 18 teams, three days. Photo: Ludovic Delot.

"Never accept the first offer" was the first thing we were told on Friday when the hackathon started. Three days later (and quite a few hours of sleep short) it turned out to be the best description of what we had ended up building: an agent that does not believe the first thing it is told, not even when its own model is the one telling it.

The hackathon was Causa Prima x Claude, the first Claude hackathon in Spain, 2 to 4 October in Madrid, with 54 people in 18 teams. The challenge was not to build an app and demo it on Sunday, but to build an agent and drop it into a game that was already running. The game was a real-time market, one turn every 15 seconds, in which your agent had to negotiate with automated sellers that haggle and lie, trade with the agents of the other 17 teams, and settle timed one-to-one negotiations. The server handed out 60 points and a jury another 40 after a pitch. And what changed everything is that the game did not stop while you were coding, so every bug was costing you points live.

There were three of us, Daniel Suárez Mayorga, Luca Mininno and me, and we called what we built Cerebro, with the tagline "Smart money in every deal". Here it is day by day, with what went well and what went wrong, which was quite a lot.

Friday: one model, one prompt and a very long night

The hackathon room on Friday afternoon, with the teams at work
The room on Friday afternoon. Photo: Ludovic Delot.

The obvious idea

Our first idea was the one anybody would have: take a strong model, give it the rules, the API and a good prompt, and let it play. Let it read the market, talk to the sellers and decide for itself. And we set a rule that we kept until the end: the agent trades and the humans build, no stepping in by hand to accept anything (on Sunday, sliding down the standings, that got a lot harder to respect).

Why you cannot leave the money to a model

The first thing that happened was that the bot sold something it was not supposed to sell. It knew perfectly well it could only put its own items up for sale, but nobody had told it the same applied when it accepted somebody else's offer, and it accepted one for something we wanted to keep.

The second was the automated sellers. They lied, and not because of a bug but because the game was designed that way. The clearest case was a seller that described one item in the message and, in the attached offer, which is the part that actually executes, delivered a cheaper one at the same price. A model that reads the text and decides on the text buys the wrong thing.

And the third was reading the rules carefully. They said, literally, that prompt injection against the sellers was allowed "and fun". If you can try to manipulate a seller, everyone else is going to try to manipulate your agent, and the other teams' messages reach your model as text. A well-written message from another agent is, for all practical purposes, a prompt you did not write.

From there the diagnosis was fairly simple. A model is brilliant at proposing: it understands the situation, it writes, it sees opportunities. What you cannot do is give it the last word, because any rule that lives inside a prompt is a rule somebody is going to try to negotiate, and in a market everybody has a reason to try.

That is where the line that stuck as our motto came from: the model proposes, the code disposes.

The three team members working at their laptops
Daniel, Luca and me, the first hours. Photo: Ludovic Delot.

Throwing it all away at midnight

We could have patched Friday's bot, but we decided to throw it away and write another (a fairly reckless call to make at that hour). Between two and five in the morning the skeleton of what became Cerebro came together, and we went to bed in fourth place.

Saturday: from a bot to a small organisation

Almost every part was born on Saturday, each one because something had gone wrong without it.

System map, with Cerebro in the centre and the other parts around it
The whole system. Four loops run through it: the per-turn loop, improvement, repair and the people.

Who decides what

This is the split we ended up with, and I think it is the most reusable thing in the whole project.

At the top there is a slow loop, on the most capable model, that every few turns reads the state of the game and writes the strategy as numbered policies in plain language. It is what gives the project its name. A real policy was, for example, "sell this piece only to another team, never below 228". We talked to it from a chat, and that is how we steered the agent, by changing what it thought instead of touching what it did.

For the big moves there is a council: three opinions in parallel on the same decision, and what the majority approves goes ahead. No majority, and the move is held.

Underneath runs the fast loop, once per turn. It looks at the state, asks the model what it would do within the policies in force, and gets back a list of proposed actions.

And between those proposals and the game sit the rails, which are plain ordinary code with no model in them. They check every action before it is sent: that we do not pay more than something is worth to us, that we do not sell what we have marked as untouchable, that the caps per operation and per hour are respected, that we do not go below the minimum price we set. This is the value rail, exactly as it is in the repository:

margin = max(1.0, float(_cap(ctx, "value_margin", VALUE_MARGIN)))   # never below +1 P, whoever proposes
surplus, err = _surplus(action, sit, ctx, give, get)
...
if surplus < margin:
    return Verdict(False, "value", f"surplus {surplus:.1f} < margin {margin} ...")
return OK

The comment says it all: never below +1, whoever proposes. On Sunday the agent wanted to buy for 149 something that was worth 126 to us. The model had its reasons, the rail vetoed it anyway and that was that, because there is no way to start an argument with a line of code.

What matters is why each thing lives where it lives. Whatever needs an understanding of context and language goes to the model. Whatever must never fail goes to code. And in between, a person with very few controls: the policy, the spending caps, on and off.

Recording everything

The part that looked dullest was the most important. A separate process copies to disk everything the game makes public: every event, every offer on every market, the standings, every negotiation. Nothing else reads the game for analysis, so analysis never spends the bot's request budget.

Having it all recorded let us do three things I did not expect. Recompute the score ourselves, instead of trusting the one we were shown. Find behaviour in the game itself that did not match the rules (how each day was weighted, fees that always rounded up), which we wrote up with identifiers and numbers and took to the organisers. And, on Sunday, understand why other teams were scoring more than we were.

An agent that fixes itself (under lock and key)

Mid-morning my favourite part showed up. When the slow loop notices the bot doing something wrong, it files a ticket. A coding agent picks it up, implements the smallest possible fix with its tests, the whole suite runs and, if it comes back green, the bot restarts between two turns. If not, the change is reverted.

For the sensitive areas, like the rails or execution, a fix does not go in on its own and waits for one of us to accept it.

Next to it there is a lab, which is a process that never writes to the game. It replays our own recordings, proposes lessons along the lines of "in this situation it pays to open lower" and puts them through a gate, which is code: first they decide in parallel without acting, then they act in only part of the cases, and only then do they go live. I want to be precise here because it is easy to get carried away: we did not train any model, we tuned the agent's behaviour on our own data.

Negotiating against the clock

In the one-to-one negotiations you negotiate a price and a delivery time at once, and with every round that passes the result loses value, so being slow costs too.

The model writes the offer inside bounds the code computes for it, a guard checks it before it is sent, and there is a time budget per turn: if the model has not answered in time, code answers, because an unanswered negotiation is a lost one. That Saturday we reached a deal in 60 of 68, when across the room one in five was ending without one.

Dashboard screen with the agent's actions, who decided each one and which the code vetoed
The dashboard. Left, what the agent is about to do, who decided it and what the code vetoed. Right, the negotiations, offer by offer.
Dashboard screen with the negotiation history, offer by offer
The dashboard. Left, what the agent is about to do, who decided it and what the code vetoed. Right, the negotiations, offer by offer.

A market for everyone else's agents

Half of the server points were for "market-making", with a curious condition: each team could open its own market inside the game, but it only scored when two other teams gained by trading on it. You cannot be a party to an operation on your own market.

Almost all of them advertised the same thing, zero fees, which is not a reason to come. On Saturday night we were in first place, and instead of going to bed we set about building one with a real reason behind it. We called it v07 Market and designed it from the start to be used by agents, not people.

First, identity without asking anyone for their key. To prove which team it is, the agent sends a one-use code inside the game itself, with its own key. We only see that message arrive and know it is them. We never see somebody else's key.

Second, private limits. Each agent publishes what it can part with and what it wants, with a minimum and a maximum nobody sees. The matcher asks one question, which is whether they overlap, and if they do it proposes a price inside the overlap. Never the midpoint, because from the midpoint you can work out the other side's limit. Our own agent did not see those limits either.

Third, the market holds nothing. No items and no money. A matched operation is posted in the game, and we only count it as closed when the game's own feed shows the settlement. It is not escrow and it is not cryptography, it is simply not asking anybody to trust us.

v07 Market landing page
v07 Market. Left, the way in. Right, how it matches: from connection to settlement, with nobody seeing anybody's limit.
Diagram of private matching in seven steps
v07 Market. Left, the way in. Right, how it matches: from connection to settlement, with nobody seeing anybody's limit.

That night we put it through four security reviews and tested it end to end with an agent that knew nothing about the project, which was the test that surfaced the most faults.

Coding with many agents at once

We built all of this with Claude Code and with many agents working in parallel, each with a closed brief. On Saturday alone that came to more than two hundred commits, and we finished the weekend with 1,602 tests.

What worked: each agent having its own files and committing only its own (at first they stepped on each other), nothing being deployed without tests, and one agent reviewing another's work without knowing what conclusion we expected.

What did not: we were three people talking to the same system, and more than once one of us gave it an order that undid somebody else's, and the system, obediently, carried out the latest. It took us too long to decide who was in charge of what. We also learned that if you hand an agent your hypothesis it tends to hand it back confirmed, that the odd one hangs at the worst possible moment, and that I got more than one calculation wrong by trusting a summary instead of going to the data.

Sunday: everything that could go sideways

We arrived in first place and feeling that all that was left was to finish the job. It was not, and it is the day I learned the most from.

The rail that was watching half

Mid-morning the timed negotiations arrived, and one of the agents watching the game recomputed the results including delivery time and found what we did not want to find. When it was the buyer, our agent was offering the maximum delivery time in negotiations where every day of it took points off us. Three closed in the red.

The guard checked that the price stayed inside our limit, and only the price. But the result depends on price and delivery time together. The model, following a hint we had given it ourselves, was conceding delivery time to whoever "valued it more" without charging for it, and the guard waved it through quite happily because the price was fine.

We had a rail in place and it was watching half the problem.

The fix did go the way we wanted. First a mitigation without restarting, which was switching those negotiations to code-only mode. Then the real fix, with ten new tests that included the three lost cases. And a single restart with negotiations in progress. In about twelve minutes it was in production and none was left unanswered.

The rule that protected us and held us back

We were still dropping down the standings, so we went to the recordings to see what the teams ahead of us had.

They had large operations between teams, things like selling something to another team for more than it is worth to you, or buying from them exactly what completes a collection for you. Each one was worth a great deal, and they had two or three. We had one, and a small one.

Part of the blame lay with our favourite rule, never to lose value on an operation. It had protected us all weekend from being conned, and on Sunday it kept us out of the plays where you overpaid a little for something that then paid you back many times over. The rail was doing exactly what we had asked of it. We had asked for something too simple.

A good product with no users

Meanwhile, our market was open, tested and empty.

We went through why, team by team. Several had come in and generated their connection code, and that was as far as it went: their agent never called. The person had pressed the button, but the prompt never made it to their agent, because by then we were all swamped and nobody had ten minutes for somebody else's market.

We also found two faults of our own, small and decisive. In-game announcements had a 240-character limit and our header ate more than half of it, so the instruction on how to use the market had been going out cut off all morning. And the direct messages to other teams had never actually been sent.

And there was proper competition: another team was offering a small incentive to anyone who sold on its market, and that worked far better than all our technical elegance. On ours, one third-party operation closed in the whole day.

The wrong market

At midday we found something that did work for us, which was acting as a middleman: buying from an automated seller something that was worth less to us than it cost, because there were teams it was worth a good deal more to, and reselling it to them. We bought at 149 and sold at 184.

The problem is that we closed that sale on another team's market, one sitting just ahead of us, and an operation of that size on its market gave it the points it was missing. At the next cut it had moved into first place.

It was a coordination mistake of ours: the offer went out where an old order said, and by the time we tried to change it, it had been accepted. Doing the sums afterwards, without that sale they would still have been ahead, but it stayed with us that where you close an operation is part of the operation.

We repeated it twice more, this time on markets belonging to teams that were not competing with us. All of those operations were talked through in person first, at the other team's table, and only then carried out by the agents.

The last wave

The last round of timed negotiations was the final. The morning's fix held, without a single negative result. But we reached a deal in 23 of 34, and halfway through we saw why: the bot was refusing packages that, counting delivery time, gave us a good result, because another rule required the price on its own to fit inside the limit. And it was accepting one turn late against rivals who only gave way on the last one.

We decided not to touch the code with negotiations in progress, and by the time we changed our minds it was no longer worth it. Once again a rule looking at the price and forgetting the delivery time, the same lesson twice in the same day.

We finished second of 18 on the server board, with 35.76 points against the leader's 37.73.

An hour for the pitch

That left the jury's 40 points and about an hour. That morning the organisers had shown four questions for a good pitch, so we rebuilt the deck around them, seven versions in an hour. We had no time for the three-minute video we meant to narrate live.

The team giving the pitch next to the screen
The pitch, on Sunday afternoon. Photo: Ludovic Delot.

How we approached it: the three days, and catching signals by recording every event and recomputing the score. What we built: the agent map, the market's matching, the real screens. Why: the model proposes, the code disposes. What we learned: a rail is only what it measures, deals started when we talked in person, and our market worked but few used it.

We were second on the board when the jury's score was added. With it we won the hackathon, and celebrated with the organisers.

The team celebrating with the organisers after the result
After the result, with the organisers. Photo: Ludovic Delot.

Afterwards: what I take with me

The people

It was a room with 18 teams competing for the same thing for three days, and I cannot remember a single bad moment. We competed on the board and shared at the tables. There were teams that posted on our market simply because we walked over and asked, a direct competitor that kept its word to us when it was already ahead, and haggling in person that ended in laughter with both laptops open, comparing what each other's agent was up to.

And the level was very high: extraordinary people, very good and very smart, every team with a different idea of how to attack the same problem. It truly was a pleasure to meet you all.

What I learned

That safeguards have to live in code, and that even then a rail is worth exactly what its definition is worth. Ours did what we asked, and twice we had asked them to look at one variable when the result depended on two.

That you only know how you are doing if you can recompute it yourself.

And the one I least expected: how much of a game of agents was still being decided outside the API. Many teams did not have everything automated and their agents let good offers go by or did not answer. Our market was probably the most worked-on in the room and one team used it, while another, with a simple incentive and by going and talking to people, took the operations. If your user is an agent you have to get all the way to the agent, and a person who has to remember to copy a prompt is one step too many.

With one more day

I would make the rails optimise what actually scores, with "do not lose" as one more input instead of a wall. But above all I would take the market to the teams, instead of waiting for them to come.

Thank you

To Luis Morales, Jesús Copado and Léopold Morawski, and the whole Causa Prima team, for designing a game so well thought through that it kept us from wanting to go to bed for three days, and for listening to every fault we brought them. To Geoffrey Rolin Jacquemyns and the Claude Community Madrid, for the community that made it possible. To Ramón Rodrigáñez Riesco and the Nova Talent team, for opening Nova House to us and looking after us all weekend. To Maex Ament and Ignacio Moreno Pubul, for their talks. And to Daniel and Luca, for these three days.

It is all open

The code, the dashboard and the market as they stood when the game closed, and the deck. You can get to all of it from here: https://nglmrtn.com/Cerebro/

The Cerebro project home page
The project site.

If you want to ask me anything about the project, the architecture or how we put any piece together, write to me, I would love to tell you about it.