In Nigerian towns, hiring a tradesperson runs on word of mouth. You ask in a group chat for someone who can fix a breaker or clear a blocked drain, and you get a phone number. If the person takes your money and stops answering, you have no recourse and no record that it happened.
Gigr is a marketplace that replaces that with something accountable. Every user gets a wallet on signup. Money sits in an escrow contract and is released when work is confirmed, in milestones when a job has stages. On completion the artisan receives a vouch, which is a compressed on-chain certificate proving a settled job actually happened. A vouch cannot be bought or faked because it can only be minted through an escrow transaction that moved real money.
On top of that sits Gigidy, an assistant you talk to instead of filling in forms. Say what you need and it finds providers, drafts the request, and holds your budget.
This post is about the problem we hit building the assistant, and what we had to do to fix it.
The assistant could not remember anything
The first version was stateless. Each conversation started from nothing, so the agent asked again for a budget the user had given yesterday and a neighbourhood they had already named. Worse, it could not learn from outcomes. A user who hired someone who did poor work got no benefit from that experience in the next job.
We had been storing a small amount of this in a JSON column on the user row. That is not memory. It is a settings table. It cannot be searched by meaning, it cannot hold a hundred observations without becoming a mess, and it belongs to our database rather than to the person.
What we moved it to
Walrus Memory is a memory service built on Walrus, which stores data as blobs on the Sui blockchain. Each user gets their own namespace inside our account, so one person's memories are never recalled against another's.
The agent now writes facts about a user there and recalls them by meaning rather than by key. When someone asks for a plumber in Akoka with a budget of fifteen thousand naira, the search returns sentences about their past jobs and their area rather than requiring us to know in advance which field to look up.
Two decisions shaped how this works. First, facts are produced by templates over real database rows, never generated by a language model. A completed job produces a sentence because the job has a category, a price and a year. If the row is missing a field, no fact is written. A guessed memory is worse than no memory, because the user has no way to know it was guessed.
Second, memory grows with use. Early in development we backfilled existing history, and the facts we have now are a snapshot rather than a fixed number. The number changes every time someone completes a job.
Deletion on a store that does not delete
Walrus is immutable by design. A blob is an object on Sui, and you cannot edit a field inside it. When we started, the Python SDK had no delete method at all, so we filed an issue describing it. Mysten reproduced it and shipped forget in version 0.2.2 of the SDK.
That method deletes a whole namespace, which is not what users want. A user who wants one memory removed, or one fact corrected, is asking for something the storage model does not offer.
We built it on top of what exists. To delete one fact, the service reads every memory in the namespace, works out what should remain, deletes the namespace, and writes the remainder back. The dangerous moment is the gap between the delete and the rewrite. If the process dies there, the user has lost everything.
Before deleting anything, we write the exact list of what must be restored to a journal file on disk, using an atomic rename so a half-written file cannot be read. Every later operation finishes any pending restore before doing its own work. There is a test that kills the process in the middle of the rewrite and proves a new instance repairs the namespace on the next call. You can read that test at test_memory_reconcile.py.
The interface does not pretend this is free. The confirmation tells the user that the memories they keep will be stored again with today's date, because that is what actually happens when you rebuild a namespace.
Reading the chain through Tatum
Tatum is a blockchain infrastructure provider, and we use it in three places across two chains.
On Solana, account and blockhash reads go through Tatum's RPC gateway. Each read has a short timeout and one fallback if it does not answer. Writes never go through Tatum. Routing a transaction through any third party is how you break escrow, and we had already learned that the hard way.
On Sui, verification reads the Walrus blob object through Tatum's Sui gateway and reports its certified epoch. This one is gRPC rather than JSON-RPC, because Tatum's Sui JSON-RPC endpoint does not exist, and Sui itself has deprecated public JSON-RPC in favour of gRPC and GraphQL.
The particular blob we verified reads as object type 0xfdc88f7d7cf30afab2f82e8380d11ee8f70efb90e863d1de8616fae1bb09ea77::blob::Blob at version 810023862, which you can open on a Sui explorer rather than taking our word for it.
The third use is verifying payments, which is the last section.
Selling to software
The marketplace has a second kind of customer that is not a person. An AI assistant working for someone else may want to know whether a provider is reliable before recommending them. That assistant has no email address and no card, and it cannot complete a signup form.
x402 is a payment scheme built on the HTTP 402 status code, which was reserved in 1997 for exactly this and almost never used. When a program asks Gigr for a brief on a provider, the endpoint answers with a price instead of a login wall. The program pays in USDC, a dollar-pegged stablecoin, and retries with the settlement signature. Gigr reads that transaction on chain and checks the token, the recipient, the amount, and that the signature has not been spent before. One payment buys one brief.
A detail worth stating: x402 does not support Sui at this time. Its supported networks include Solana, but not Sui, so a payment cannot settle on the chain the rest of this system uses. We built it on Solana because that is where it works, and we say so rather than implying otherwise.
What we did not get right
The deleted memory is not erased from Walrus. The blobs persist; the rows stop being retrievable. We describe it that way in the interface because claiming otherwise would be a lie a user could not check.
What is there to look at
The code is at github.com/0GigR/GigR with 477 backend tests and 295 frontend tests, and CI runs are at github.com/0GigR/GigR/actions. The application is at gigrr.vercel.app. A walkthrough with the demo video and every component named here is at gigrr.vercel.app/judges.
We filed five issues against Mysten's SDK and relayer while building this, and all five are closed:
-
Python SDK has no forget or delete method, shipped as
forgetin memwal 0.2.2 - recall() cannot pass scoring weights
- recall() drops the created_at and score fields
- Python SDK defaults server_url to localhost
- An idempotency key reused under a different namespace returns a 500 instead of a conflict, fixed in relayer 0.1.0 after they confirmed the cause
Top comments (0)