A program can pass every local test and still be a long way from ready for real users.
It needs to be deployed to the right network, with sensible controls over who can change it. Other developers need a reliable way to call it. Users need a frontend that can connect to their wallet and submit transactions. When those transactions fail, the app needs to explain what happened.
Arc 13 covered that last stretch.
Across Days 85–91, we deployed a program to mainnet-beta, secured its upgrade authority, published its interface, generated a typed client, built a wallet-connected frontend and improved the way it handled transaction failures.
Then we made the launch public, with an on-chain address anyone could verify.
Deployment changed the stakes
Until Day 85, most of our work had taken place locally or on devnet.
Those environments gave us room to experiment. We could redeploy, replace accounts, request test SOL and recover from mistakes without putting real assets at risk.
Mainnet-beta is different.
The program becomes public infrastructure. Clients may begin depending on its address and interface. Users can send transactions involving real assets. Any authority left attached to the program now controls production code.
Mainnet deployment also costs real SOL. The deployment wallet needs enough to create and fund the on-chain program account, with the amount depending on the size of the program.
The first challenge was therefore about more than learning another command.
We built the program, configured the CLI for mainnet-beta, funded the deployment wallet and deployed the program to the live network.
Once it was there, we verified the program ID through Explorer rather than assuming that a successful command meant everything was correct.
That verification matters.
A frontend configured with the wrong address may call an older deployment or fail entirely. Documentation pointing to the wrong program creates the same problem for every developer who follows it.
The deployed address is the identity that clients and users will rely on.
By the end of the challenge, we had something we had not had before: a public program running on mainnet-beta at an address anyone could inspect.
Mainnet deployment was an operational decision
The Web2 comparison throughout the arc was moving a service from staging to production.
That comparison holds up because deployment changes who can rely on the software and what a mistake can cost.
A staging service can usually be reset.
A broken development build can be replaced without affecting users.
A public interface may already have clients depending on its exact behaviour.
The deployment command is only one part of that transition.
We also need to know:
- Which wallet paid for the deployment?
- Who can replace the program code?
- Is the live program ID recorded correctly?
- Can another developer discover the interface?
- How will users submit transactions?
- What will they see when something fails?
- Is there verifiable evidence of what was deployed?
Those questions made Arc 13 feel less like another coding arc and more like the beginning of operating software.
The upgrade authority was a production credential
When a Solana program is deployed, its upgrade authority initially belongs to the deployment wallet.
That authority can replace the executable code while keeping the same program ID.
This is useful during active development. Bugs can be fixed and new functionality added without forcing every client to move to another address.
It is also a significant amount of power.
Anyone who controls the upgrade authority can change what the program does.
That makes it closer to a production credential than a normal development key.
On Day 86, we inspected the authority attached to our program, transferred it to another key and then transferred it back.
The exercise made the relationship concrete.
The program might be visible at a public address, but whoever holds the upgrade authority can still replace the code behind that address.
Leaving it attached to a personal wallet creates a single point of failure.
The key could be lost or exposed.
Its owner could become unavailable.
A mistaken command could transfer authority to the wrong address.
For a real production program, the safer approach is usually to place upgrade authority behind a multisig such as Squads.
An upgrade can then require approval from several authorised people rather than depending on one private key.
That also gives the team a clearer release process.
Who proposed the upgrade?
Who reviewed it?
How many approvals are required?
Which build is being deployed?
A multisig does not guarantee that an upgrade is correct, but it reduces the chance that one compromised wallet or accidental action can change the program.
We could also remove the upgrade authority
The other option was to make the program immutable.
Removing the upgrade authority with --final permanently prevents the program from being upgraded.
That can be a strong guarantee for users. The code they inspected cannot later be replaced by an authority holder.
But the decision cannot be reversed.
If a vulnerability appears, the program cannot be patched at its existing address. A replacement would need to be deployed, and clients and users would need to move to it.
That makes immutability a governance and operational decision rather than a final bit of deployment housekeeping.
Before removing the authority, a team needs confidence in the code, the tests, the interface and the plan for handling future problems.
For most actively developed programs, moving the authority to a well-managed multisig is a more practical first step.
Immutability may come later, once the inability to change the program is a feature rather than a liability.
A program ID was not enough
Deploying the program made it available, but it did not make it easy to use.
A program address tells us where the code lives.
It does not tell another developer:
- Which instructions the program exposes.
- Which arguments those instructions accept.
- Which accounts each instruction requires.
- Which accounts must sign.
- Which accounts must be writable.
- How program-owned account data is structured.
- Which errors the program may return.
Without that information, integrating with the program would mean reading its source code or manually reconstructing its instructions and account layouts.
Day 87 was about publishing the interface.
We rebuilt the Anchor program, published its IDL and fetched it back from the network.
The IDL describes the program’s public surface in a machine-readable form.
The Web2 comparison is an OpenAPI schema.
An API may be reachable at a URL, but developers still need to know its routes, parameters, request bodies and responses.
An IDL plays a similar role for an Anchor program.
It turns a program address into an interface that tools and developers can understand.
The IDL became a contract
Publishing the IDL did more than save another developer from reading our source code.
It made the program interface explicit.
That matters once frontends, scripts and other programs begin depending on it.
Changing an instruction name can break generated clients.
Changing its arguments can break serialisation.
Adding a required account can make existing transactions incomplete.
Changing an account layout can make stored state unreadable to older clients.
During local development, those changes are easy to treat as implementation details.
After deployment, they become compatibility decisions.
The IDL records the interface that other software expects.
That does not mean the interface can never change. It means changes need to be considered, versioned and communicated like changes to any other public API.
Codama turned the interface into a typed client
Once we had the IDL, we used Codama to generate a TypeScript client.
Without a generated client, calling a program can involve a lot of manual work.
The developer needs to serialise the instruction arguments correctly, provide accounts in the expected order and interpret account data using the right layout.
A typed client moves much of that work into generated code.
Instruction names, account requirements and data types become available through normal TypeScript tooling.
Editors can offer autocomplete.
The compiler can catch some incorrect arguments before a transaction is submitted.
Developers do not need to guess how an account should be decoded or how an instruction should be assembled.
Generated clients do not remove every integration error. A correctly constructed transaction can still fail because the signer lacks authority, an account contains unexpected state or the network has moved on since the transaction was prepared.
They do remove a large class of avoidable interface mistakes.
The deployed program gave us an address.
The IDL described the interface behind it.
Codama turned that description into code another application could use.
We took the wallet flow back to devnet
The next challenge moved from program integration to user interaction.
For this exercise, we returned to devnet.
That let us build and test the browser wallet flow with test SOL before applying the same pattern to a production application. It also reinforced an important operational point: the network, RPC endpoint and program address need to be treated as configuration rather than buried throughout the frontend.
Using the official Solana dApp template, we built a React app that could:
- Discover compatible wallets installed in the browser.
- Ask the user to connect one.
- Display the connected wallet’s devnet address.
- Fetch and show its devnet balance.
- Build and send a small SOL transfer.
None of those operations was new on its own.
We had generated keys.
We had read balances.
We had built transactions.
We had submitted transfers.
The difference was that the authority now came from a user’s wallet through a browser interface.
The app prepared the transaction, but it did not hold the user’s private key.
The wallet showed the request and asked the user to approve it.
Only then could the transaction be signed and sent.
That is the interaction model users expect from a Solana application.
Wallet Standard avoided wallet-specific integrations
A frontend could integrate separately with Phantom, Solflare, Backpack and every other wallet it wants to support.
That would create a growing collection of wallet-specific code and assumptions.
Wallet Standard gives browser wallets a shared interface.
Instead of hardcoding a list of supported products, the application discovers compatible wallets available in the user’s browser.
A wallet can advertise its capabilities through that common interface. The application can then connect, request signatures and submit transactions without building a separate integration for every wallet.
That makes the frontend easier to maintain and gives users more choice.
A user should not have to install the one wallet the developer happened to choose.
The application should work with wallets that support the standard.
Connecting a wallet did not mean controlling it
The frontend work also reinforced an important boundary.
A connected wallet has not handed its private key to the application.
The connection gives the app access to public information, such as the selected address, and a way to request approved actions.
The wallet remains responsible for signing.
When the user sends a transfer, the app builds the transaction and presents it to the wallet.
The wallet shows the request.
The user approves or rejects it.
The wallet signs only after approval.
This separation matters for security and user trust.
The app can guide the interaction, but the wallet controls the authority.
It also means that a transaction can fail before it reaches the network.
The user can reject the request.
The wallet can disconnect.
The signing window can close.
The application needs to handle those outcomes just as carefully as an on-chain error.
Transaction failures needed better explanations
The first version of a wallet flow often handles success and treats every failure in the same way.
Something went wrong.
Try again.
That is not enough for a production application.
A user who rejected a signing request does not need to be told that the network failed.
A user with insufficient funds needs a different next step from someone whose transaction expired.
A disconnected wallet should be reconnected rather than asked to repeat the same transaction immediately.
Day 89 introduced a reusable error-classification layer around wallet transactions.
It distinguished between cases such as:
- The user cancelling the wallet request.
- The transaction’s blockhash expiring.
- The wallet lacking enough SOL.
- The wallet disconnecting.
- An unknown wallet, RPC or program failure.
The original error still went to the developer console.
That preserved the detail needed for debugging.
The user saw a shorter explanation and, where possible, a useful next step.
The application needed both views.
The developer needed the logs, RPC response and program error.
The user needed to know what happened and what they could do about it.
Different failures needed different responses
Some failures happen inside the wallet interaction.
The user declines to sign.
The wallet is locked.
The selected account changes.
The connection disappears.
Others happen while preparing or sending the transaction.
The balance is too low.
The recent blockhash has expired.
The RPC endpoint cannot be reached.
The network rejects the transaction.
Another group comes from the program itself.
An account fails an Anchor constraint.
The signer lacks authority.
The requested state transition is not allowed.
Checked arithmetic returns an error.
These failures may all surface from the same transaction flow, but they do not mean the same thing.
Classifying them makes the app easier to use and easier to support.
It also prevents misleading advice.
Telling someone to reconnect will not fix an insufficient balance.
Telling them to retry will not help when they deliberately cancelled the request.
Telling them only that the transaction failed gives them nothing useful at all.
The pieces had to work together
Day 90 continued the production-readiness work ahead of launch.
By this point, the program was deployed, its interface could be published, a typed client could call it, and a browser wallet could approve transactions.
The remaining concern was whether those pieces agreed with one another.
The frontend needed to point to the intended cluster.
The client needed to match the deployed interface.
The program ID needed to be correct for the selected environment.
Wallet and transaction state needed to remain clear when users connected, disconnected or encountered an error.
That was the wider lesson of the arc.
Deployment was not one command at the end of development.
It was the work required to make the program operable, integrable and usable.
The launch post made the deployment verifiable
The final day asked us to make the launch public.
We wrote a post explaining what we had built and linked to the deployed program in Explorer.
That link gave the announcement verifiable proof.
Readers did not have to take our word for it that the program was live.
They could inspect the address and confirm that the executable program account existed on mainnet-beta.
A useful launch post could also include:
- What the program does.
- Why we built it.
- The mainnet program address.
- A link to the source code.
- Instructions for trying the application.
- A screenshot or short demonstration.
- An invitation to test it, contribute or ask questions.
The aim was not to produce a grand launch campaign.
It was to mark the point at which the work became available to other people.
That changes the nature of the project.
Before launch, the code mainly needs to make sense to its author.
After launch, users need to understand what it does.
Developers need to understand how to integrate with it.
Contributors need to understand how to work on it.
Operators need to understand how it can be upgraded and supported.
Publishing the work begins that conversation.
What Arc 13 taught us
Arc 13 moved us from building a Solana program to operating one.
By the end of the arc, we had learned how to:
- Deploy a program to mainnet-beta and verify its live address.
- Treat deployment as an operational decision with real costs.
- Inspect, transfer and secure upgrade authority.
- Understand the trade-off between upgradeability and immutability.
- Publish an Anchor IDL and generate a typed TypeScript client.
- Connect a React application to browser wallets through Wallet Standard.
- Keep private keys inside the wallet while requesting approved transactions.
- Classify transaction failures and give users useful next steps.
- Keep network, program and client configuration aligned.
- Publish a launch backed by verifiable on-chain evidence.
The main lesson was that deployment does not finish when the program reaches mainnet.
A production program needs clear ownership.
Its upgrade authority needs to be protected.
Its interface needs to be discoverable.
Clients need a reliable way to call it.
Users need a safe way to approve transactions.
Failures need to be understandable.
The address in Explorer proves that the program is live.
Everything around that address determines whether anyone can use it with confidence.
Revisit the Arc 13 challenges
Day 85: Deploy your program to mainnet-beta and verify its live address
Day 86: Inspect and secure your program’s upgrade authority
Day 87: Publish the IDL and generate a typed TypeScript client
Day 88: Build and test a wallet-connected React frontend on devnet
Day 89: Turn wallet and transaction failures into useful error messages
Day 90: Bring the deployment, client and frontend pieces together ahead of launch
Top comments (0)