Guayaquil, ECUADOR — Rogelio Bolufé stared out the porthole of the white Boeing 767 as the landing gear hummed to life. He eyed endless green fields, a sparkling expanse of water, and a sprawling city along a river. The 44-year-old Cuban, who’d lost months of his life inside the vast network of U.S. immigration jails, was staring at his new purgatory: southern Ecuador.
His mind wasn’t on the reality of being stranded in this strange place; that despair would swallow him later. Instead, his thoughts were fixed on the piles of documents that had been taken by prison guards and vanished.
And now, he thought, I disappear too.
His deportee companions, mostly returning Ecuadorians and men from neighboring Peru, talked themselves into a frenzy over how they’d show journalists on the ground the deep and dark impressions on their wrists — evidence that they’d been tightly bound for more than 24 hours in hot and mostly stationary buses and planes with little water and infuriating discomfort. But when the plane landed at Aeropuerto Internacional José Joaquín de Olmedo minutes before noon on June 1, the tarmac was empty. Bolufé heard one man ask: “Don’t people worry about us?”
He was still wearing his dark green prison clothes when he stepped into the scorching midday sun, down the stairs and onto the runway. An American man in uniform handed him a sheet of paper.
“Pasaporte de emergencia,” it read. “Ecuatoriano nacido en Ecuador.”
Bolufé shot the man a confused look. The document was a blatant lie. Bolufé was not an “Ecuadorian born in Ecuador.” The Department of Homeland Security knew that. It was in his case file and detention records and plastered all over the internet thanks to his minor celebrity status in political circles back in Miami’s Little Havana.
But it had been days since he’d slept. And Bolufé had just ended an 11-day hunger strike over worsening detention conditions back in the United States, a protest he suspected had landed him this one-way trip to one of the most notorious cities in South America. In a split second, Bolufé decided he would not object. There was no way he’d wait for this to sort itself out while trapped at a local jail cell in the narco cartel city of Guayaquil. That would be a death sentence.
He handed the paper to an Ecuadorian immigration officer (who kept it), nodded when asked to verify the details, and was soon handed a blue drawstring bag from the Red Cross. Bolufé made his way to the airport’s passenger waiting area and sat on a black chair in the shadow of a silvery escalator. He pulled a bottle of milk from the bag and downed it in seconds.
Bolufé retraced his steps to the airport’s passenger waiting area, where he sat alone upon arriving in Ecuador.
Jose Pagliery/NOTUS
For the next half hour, he considered his options. The U.S. government had ditched him in a foreign country with no money, no cellphone and no passport. His drab prison uniform, disheveled hair, and two-month scraggly beard made him unapproachable. His only identification was a green prison wristband and a CoreCivic commissary card — not official and, if anything, red flags.
Rogelio Enrique Bolufé Izquierdo — a Cuban man who had made his way through the archipelago of U.S. immigration detention centers, surviving the swampy dangers of Alligator Alcatraz in Florida and the frigid cells of an El Paso jail in Texas and the spreading diseases at Camp East Montana — was stuck.
* * *
Few Americans think of Ecuador, but when they do it’s usually in transit to the country’s national treasure: the Galápagos Islands. Lesser known is Guayaquil, a bustling port city that was once the global gateway for chocolate beans — historians recall how the thick and sweet aroma of drying cacao once hung over the entire town a century ago. Now, it’s probably where your bananas come from.
Guayaquil is situated 50 miles up the Guayas River from the Pacific Ocean, which makes it a key stop for cocaine cartels in nearby Colombia and Peru, and so the city has recently been overrun by crime.
In January 2024, the city briefly captured attention when more than a dozen gangsters with shotguns, submachine guns and sticks of dynamite took over a live broadcast of a local TV news station. By then, the nation that once boasted the lowest crime level in the region had become the most dangerous in the southern half of the globe. Guayaquil and the city of Durán, just across the river, have a combined homicide rate of roughly 90 per 100,000 — significantly higher than the District of Columbia’s darkest days in the 1990s. Even Guayaquil’s mayor, Aquiles Álvarez, has been arrested on corruption charges — and recently swore he’d keep leading from behind bars. (Last week, he received a three-year prison sentence.)
Even the airport isn’t safe. In mid-June, the leader of a gang called Los Águilas had just left the airport when two teens holding flower bouquets walked up and shot him dead.
But that only hints at the deteriorating situation on the ground, where thugs demand vacunas — which literally translates to “vaccines” but refers to the tolls paid to cross gang-controlled streets, a foreign concept for Ecuadorians that only appeared with the recent arrival of Tren de Aragua gangsters from Venezuela.
Soldiers patrol the streets of Guayaquil, Ecuador, during a state of emergency declared by President Daniel Noboa as part of his effort to combat organized crime.
Dolores Ochoa/AP
Police stand guard where soccer player Mario Pineida was killed at a butcher shop in Guayaquil in December 2025.
Cesar Munoz/AP
Politically, Ecuador has become an echo of the United States. President Daniel Noboa has drawn public ridicule over his obsession with President Donald Trump: Both are rich guys who own a ton of shiny real estate and ran on promises of returning their countries to greatness.
Ecuador is also undergoing a nativist phase, blaming its problems on Venezuelan migrants. Bolufé and others deported there face a government that accepted them to acquiesce to a United States whose power looms large: Ecuador essentially has no control over its monetary policy, having ditched its currency 26 years ago and adopted the U.S. dollar.
Under Noboa, a U.S. citizen born in Miami, the country has become something of a client state — willing to accept scores of deportees like Bolufé but also giving American security forces broad license in the jungle.In June, he signed a decree granting U.S. troops immunity from criminal charges when going to war with alleged drug traffickers. The move came just months after a joint operation blew up a compound that locals professed was actually a dairy farm.
The U.S. government has designated the rival gangs Los Choneros and Los Lobos as foreign terrorist organizations. And the State Department has warned Americans not to travel north or south of a street that splits Guayaquil in half due to terrorism and crime — essentially making the entire city a no-go zone.
That profile makes it a curious deportation destination, as it’s become for more than 1,800 Cubans since the start of the second Trump administration. The strategy is a consequence of White House deputy chief of staff Stephen Miller’s mission to rid the U.S. of undocumented people as quickly as possible, even through “third-country removals.” The July 2025 agreement between the United States and Ecuador pledges to seek a “dignified, safe, and timely transfer.” In reality, deportees’ final stop is a perilous place.
“It’s ‘plata o plomo,’” one former diplomat told me, echoing Pablo Escobar’s famous ultimatum: silver or lead.
“You either turn your eyes and get paid to look the other way or they kill you — and they start with your family,” the diplomat said. “It rivals the worst of conflicted places in Mexico.”
* * *
Bolufé knew he was in dire straits as soon as he walked out of the Guayaquil airport. He had no money, but he couldn’t receive a bank wire without identification. He couldn’t contact his ex-girlfriend via social media to send documents or money, because he couldn’t log in from a new and distant IP address without an incoming text to verify it was him. He couldn’t start a new online account without a phone number. And he couldn’t buy a phone.
He’d always been a hustler, having sold DirectTV and internet-connected computers growing up in communist Cuba. Now his old ways kicked in. He decided he’d offer a taxi driver money he didn’t have. He strode past the Japanese koi pond and palm trees and made a straight shot for the back of the airport taxi line, betting that the last driver was most pressed for time. He played it cool and offered the man a hefty $20 an hour for a four-hour tour of the city.
They first stopped at the Sheraton and its pristine lobby decked out with modern art, where a concierge took one look at Bolufé’s prison uniform and informed him no rooms were available. Bolufé continued behind the lobby to look up other hotels on a public computer — but also to Google himself.
In the insular world of Cuban expatriate politics, Bolufé is a much-maligned enigma. Conservative blogs claim he was once a member of Fidel Castro’s personal security team and an agent for the Interior Ministry, which controls internal politics with secret police. Meanwhile, the Cuban government has branded him a terrorist and accused him of “fomenting, financing, and executing actions of sabotage” against the authoritarian regime there. Bolufé swears the truth is somewhere in between, saying he was part of a special class of insiders close to the Castro family who went by the name Departamento 0, an off-the-books task force authorized to eliminate bureaucracy by “zeroing out” bad policies — think DOGE, not assassinations.
Bolufé shakes hands with Fidel Castro in 2016, shortly before Castro’s death.
Courtesy of Rogelio Bolufé
The next stop was the Marriott, where the lobby smelled like vanilla and a rack of Panama hats lined the left wall. By then, the taxi driver knew something was wrong. Bolufé had borrowed his phone to reach out to his ex-girlfriend, Haymel de la Vega, who promptly sent a picture of his Cuban passport while a cousin promised to wire funds. That image was enough to book a room, but the driver was still in the lurch. To calm him while the wire made its way south, Bolufé offered to buy a round of beers at the hotel bar — with money he didn’t yet have. They ordered Cerveza Club Premium and sat at a glossy wooden table for two in the corner, beneath a white geometric wall where the uplighting cast sharp shadows. Bolufé remembered discussing “banal, trivial things” for hours until the driver was alerted by phone that the wire had come through. They snuck out of the restaurant and left an open tab while they picked up the cash.
It was well past midnight by the time Bolufé managed to pay the driver, hotel and bar. He took the elevator to the Marriott’s top floor and sat by the pool, “just to appreciate the beauty of it.” He was still in his prison uniform. He hadn’t bathed in two days, and once again, he was penniless. He spent an hour on a lawn chair by the pool’s bright green astroturf deck, staring at the mirror-like water and imagining what it would feel like to jump in.
The next two weeks were a dizzying maze as Bolufé moved to cheaper and cheaper lodging to match the trickle of funds coming in from friends.
On Day 2, a faraway buddy used a shady online broker to order a smartphone that died hours after Bolufé picked it up. He ended up acquiring an “ENV,” a local brand that features a spartan helmet on the back. He grew wary as he took stock of the situation in the streets. Armed robbery is such an everyday affair that no sensible person wears a backpack on their back, and even old ladies wear them like stomach pouches. Private security guards in vests stand by the doors of coffee shops, grocers and the indigenous artisanal market downtown. And given that robbers nearly all ride motorcycles for a quick getaway, pedestrians on the sidewalk reflexively head further from the road when they rev by. It was no wonder the country’s president had decreed an “internal armed conflict” with nearly two dozen criminal groups labeled terrorists.
On Day 10, he managed to open an account at a local bank by bribing the teller with the bulk of the $120 a friend had sent. Transaction records on the app show he gets about $20 a day, usually from the same dedicated friend.
It wasn’t until Day 17 that another friend flew in from Miami — and was too nervous to leave the hotel. He dropped off a suitcase with Bolufé’s Cuban passport and a few clothes — black sweatpants and a Hawke & Co. jacket. Someone had heard it was winter south of the equator. Technically, yes, but in Ecuador that means 90-degree days and constant rain.
One of Bolufé’s half-dozen stops was a tiny Airbnb apartment that gave him the creeps. His suspicions turned out well-founded: One night, just before midnight, he was watching Netflix naked in bed when a man barged in, dressed all in black. Bolufé reached for a chef’s knife in the kitchen. “Well, that’s it, we’re ready to die,” he said, and pushed the man out. When he hurriedly texted the proprietor, he got the sense that the man was in on it. Nowhere seemed safe.
* * *
I first came across Bolufé while researching allegations of human rights violations at Alligator Alcatraz in the Florida Everglades.
How he got there was a tale unto its own. He had once run a club in Havana, but he got stuck on a trip to Miami during the COVID-19 closures. Without a work visa, he got by on eBay selling rare Cuban literature he found at an estate sale. He remade himself in Miami as someone who books reggaeton performers, and it was after a recording session on Aug. 17, 2025, that his crew got pulled over by Hialeah cops who wrote in their report that they found “a small clear plastic baggie with white powdery substance resembling cocaine” in his pocket when he stepped out of his client’s Lamborghini. Bolufé posted bail, but Immigrations and Customs Enforcement agents grabbed him, hauling him to the new swamp detention camp that had become the butt of Trump’s jokes. “Don’t run in a straight line,” the president once said, making a zig-zag motion with his hands to advise on how to avoid getting eaten by alligators.
When Bolufé called the only number he could remember — the one belonging to his ex-girlfriend — she was bewildered.
“What are you talking about?” she said. “You don’t do drugs.”
Bolufé having coffee in Versailles, Miami’s premiere hangout spot for Cuban expatriates.
Courtesy of Rogelio Bolufé
De la Vega, a health care consultant armed with ChatGPT and a dream of one day being a lawyer, helped him write habeas corpus lawsuits in 2025. She spent hours organizing documents, figuring out how to file them and talking to reporters. Bolufé lodged an asylum claim, arguing that the same Cuban government that branded him a terrorist would harm him. De la Vega’s aspirations wilted when her ex-boyfriend’s fellow inmates begged for her help too. By the time she connected me with him, she sounded exhausted.
In the months that followed, Bolufé became my connection within the ICE detention system.
He attested to how guards were punishing prisoners by forcing them into a small cage on the tarmac of the converted airport, exposed to the swamp’s blistering summer sun and relentless mosquitos. His eyes grew wide when he described what it felt like to be chained to an eyebolt on the floor, forced to wear a thin prison outfit while surrounded by guards in parkas as the air in the massive prison tent dropped to what felt like 40 degrees.
At one point, he sent me a petition signed by him and 39 fellow detainees from Brazil, Guatemala, Haiti and elsewhere that read “I was tortured in Alligator Alcatraz.” Bolufé’s accompanying statement said, “The lights stayed on 24 hours a day, and there were constant noises of chains hitting the floor, along with headcounts up to 20 times per day, making it impossible to sleep or rest.”
Documents that Bolufé sent via mail to corroborate his story. At the top is a petition signed by him and 39 fellow detainees from Brazil, Guatemala, Haiti and elsewhere that reads “I was tortured in Alligator Alcatraz.”
Courtesy of Rogelio Bolufé
While at New Mexico’s Torrance County Detention Facility, he connected me to men who alleged mistreatment after witnessing guards suffocate to death Cuban detainee Geraldo Lunas Campos at Camp East Montana in Texas. And he flooded my P.O. box with letters and collected signatures from fellow detainees decrying the conditions behind bars at ICE facilities nationwide. He even helped organize La Union de Secuestrados por ICE, a kind of union of detainees “kidnapped by ICE.” Sketches by one inmate depict scenes from inside the compound: fungus growing on an Igloo water cooler and mold spreading from a crack in the wall to an air conditioning vent.
To pass the time, Bolufé read Nietzsche, The Alchemist, and Sun Tzu, gifts from friends on the outside. He started petitions, filed countless grievances, and went on something of a crusade demanding that the private prison contractor CoreCivic build a proper legal library at its New Mexico detention facility. Bolufé considered it absurdist humor when what eventually appeared was a tiny reading nook — with fiction books.
On May 7, guards snatched two boxes of his personal documents. Bolufé went on a hunger strike. ICE ping-ponged him across the country — a tactic he thought was an attempt to break him. Government records show he was transferred from the New Mexico jail to Camp East Montana, then to the overcrowded Arizona Removal Operations Coordination Center at Phoenix-Mesa Gateway Airport, then to the privately run Northwest ICE Processing Center in Washington, and finally to two other ICE locations in Arizona. The strangest leg of that tour was an 1,100-mile flight aboard an Embraer 145. Bolufé and a single other detainee were placed aboard the 50-seat private jet surrounded by guards — something he complained was a waste of American taxpayer funds. Gillian Brockell, an independent journalist who tracks ICE flights, pointed to government contracting price sheets that estimate the flight cost upwards of $25,000, not including fuel and insurance.
An immigration judge rejected Bolufé’s asylum claim. The stated reason for deportation was straightforward: Bolufé had overstayed his tourist visa that expired in July 2020. His journey through ICE detention came to an end when the Board of Immigration Appeals — which has been largely replaced by appointees aligned with MAGA’s maximalist deportation policies — wouldn’t reconsider the ruling.
Asked by NOTUS about the circumstances of his deportation and the inaccurate paperwork he received, DHS asserted that Bolufé has dual citizenship in Ecuador — something he immediately rejected. Bolufé said he briefly ran a bar in a Cuban neighborhood in northern Ecuador 15 years ago but returned to Cuba.
“I’m a Cuban citizen, born in Cuba on January 25, 1982 at 11:00 at night at the Línea maternity hospital,” he texted me. “I’m not an Ecuadorian citizen, nor do I have citizenship in any other country.”
As for Bolufé’s personal effects, DHS told NOTUS that “ICE ensures all of their personal property is controlled and safeguarded” but did not say what happened to his diary or other possessions. The department also said that “any claim that there are subprime conditions at ICE detention facilities is FALSE,” adding that “ICE has higher detention standards than most U.S. prisons that hold actual U.S. citizens.”
DHS did not answer questions about third-country removals to Ecuador or the safety concerns around transporting migrants to a place like Guayaquil. Instead, it offered a warning to other undocumented immigrants.
“Being in detention is a choice. We encourage all illegal aliens to take control of their departure with the CBP Home App. The United States is offering illegal aliens $2,600 and a free flight to self-deport now,” an unidentified spokesperson emailed NOTUS on Tuesday afternoon.
* * *
When Bolufé reached out to me in mid-June, he was at a cheap hotel in a terrible part of Guayaquil, calling from a number I didn’t recognize.
“I can’t even begin to explain how illegal this is,” he said in Spanish on the phone, his usual cheery tone replaced by one of utter exasperation. “I look homeless. It’s just inhumane, leaving someone with that degree of vulnerability in a country with this level of crime.”
A week later, I met him at the rundown Hotel Presidente Economy. The sagging porte-cochère faces a dilapidated street where men with hard stares stand by their off-road bikes and women spend their days building papier-mâché statues they hope to sell at Christmastime. The hotel entrance’s heavy metal doors are kept locked at all times, but the rooms have loose knobs that barely keep them shut. Windows face the corrugated tin roofs of the nearby buildings. Salsa and reggaeton blast late into the night, and the occasional gunshots barely register above the music and the street dogs that begin to howl around midnight. It’s a block north of Portete de Tarqui — that street the State Department uses to divide the scary part of town from the other scary part of town.
In Room 102, Bolufé’s clothes were piled in the corner near a pink suitcase — evidence of the thin lifeline he maintains to friends in Miami. The RCA air-conditioning unit on the wall struggled to keep up with the heat and squealed like a dying car engine. Bolufé sighed, wondering if he’d have to change rooms.
“All this moving around has me really uneasy,” he said. “I’ve had too many changes in the last few days.”
His most pressing concern was that Meta spam controls kept locking him out of his WhatsApp account for hours at a time. That tether of hope kept getting snipped.
“It’s every day, Monday to Monday,” he said, holding up his phone. “I’ve been here for 20 days, and in those 20 days, this happens every day, at all hours.”
Bolufé had $100.44 in the bank and was sporting the unmistakable thin strap of a cheap, black Casio F-91W watch. His nails were long, but his hair was short. The unkempt beard was now trimmed into a goatee of sorts. He moved slowly and shook as he spoke, chain-smoking Camel Blues while sipping the free coffee from the lobby. The machine would break the next day, but at that moment the brew was hot.
We found chairs surrounded by stone walls in a back alcove. The Ecuador-Germany World Cup game played on a TV nearby. When Ecuador scored its winning goal, car horns blared on Ambato Avenue. Cheers erupted in the streets. Bolufé took a long drag from his cigarette.
“My life has been an accident,” he said.
“I look homeless. It’s just inhumane, leaving someone with that degree of vulnerability in a country with this level of crime,” said Bolufé of his circumstances.
Jose Pagliery/NOTUS
In a life that seems worlds away from the narrow streets of Guayaquil, Bolufé posed alongside Roger Stone a few years ago at a gun show in West Palm Beach, Florida.
Courtesy of Rogelio Bolufé
He would have been busy at that moment if those jail guards hadn’t taken his personal diary. Bolufé had dreamed of leaving ICE detention — how exactly was always a hazy idea — and writing a memoir he hoped would one day echo the tales of Soviet dissident Aleksandr Solzhenitsyn in The Gulag Archipelago.
“I’m not a materialist person,” he said. “But when they took away my papers, that actually hurt. How are you going to take away the pitiful little proof I have against you? How can you fight for your rights against the very power that’s imprisoning you? They have all the best lawyers, and all that money, and here I am with my little papers.”
The conversation shifted to his past life in Cuba. Behind audacious sunglasses I keep asking him to take off, Bolufé swore he knew everybody: Fidel Castro’s adult kids — even secret ones — who fell under his watch, musical artists he discovered on the street, influential government officials. He kept insisting he was a self-made man with all the right connections. But where were they now?
Bolufé stared at his phone while scrolling WhatsApp. If they didn’t check in on him now, he said, they probably never really cared.
His primary concern was how to get by each day, which was difficult enough in a place where some coffee shops and taxis request an Ecuadorian government ID number just to hand over a receipt. But the gnawing concern in the background was figuring out how to get out of the country. The way he landed there, he feared, could make escape harder.
“I have the stigma of being deported here,” he told me, “and having been deported here is already a very bad mark against you when applying for a visa to any other country.”
Over the next few days, Bolufé mostly stayed holed up inside the hotel, too proud to say he was afraid to venture out but forthcoming enough to admit he wanted nothing to do with the battered streets of the city’s Ayacucho district. He’d become a regular at the Capitán Barbosa because it was right around the corner and the flamboyant restaurateur Ricki let him rack up a tab. One evening, an old man with a belly as round as his plate’s mound of white rice motioned for Bolufé to swing by. The viejito whispered a warning: A woman was robbed at gunpoint at our table the night before. We sat down, and my companion pulled out his ENV phone to show me something. The old man grunted and shook his head. Don’t pull it out, idiot.
One morning, I ventured to a corner bakery on the street mentioned in the State Department advisory. Battered police pickups sped by every few minutes. Rival gangs fight over these city blocks, also claimed by a little black mutt who marked his territory on the northeast corner of Portete de Tarqui and Seis de Marzo. When it was time to pay for a beef empanada, I reached into my pocket and found it empty. I glanced back just in time to see my missing $5 bill in the thick fingers of a stout man, rubbing it to examine his discovery. He glanced at the sky, and made the sign of the cross.I let him keep it.
Later, I persuaded Bolufé to guide me around the city so I could retrace his steps, first at the airport then at various hotels. During lunch at one point, he stressed the need to start planting roots at this new place he refused to call home.
“I need to socialize and make some connections,” he said. “In reality, I’m quite alone here.”
It was a Friday, and Bolufé decided he’d start at the most unlikely of places in this typically Catholic city, where nuns can be seen picking up a salty pan de ambato at pastry shops. Bolufé claims to have converted to Islam sometime during his 286-day ICE detention, having discovered the benefits of that tight-knit community behind bars. So we visited the Centro Islámico Guayaquil for its Salah al-Jum’ah midday service. The imam, speaking in Spanish but singing recitations in Arabic, said exactly what Bolufé needed to hear: The misfortunes one endures are nothing — all that matters are the teachings of Allah.
“Islam does not depend on the life and death of others,” the imam repeated.
Bolufé converted to Islam sometime during his 286-day ICE detention.
Jose Pagliery/NOTUS
Afterward, three dozen men set up a table and distributed free styrofoam containers of rice, potato salad and shredded beef.
“Óyeme, this food is great,” Bolufé chimed in, hoping to spark conversation with potential new friends. “Who’s the chef?”
“The sisters cooked it,” responded a baby-faced man to our right.
“One would hope one of the sisters is single,” said another to our left, drawing chuckles all around.
Bolufé began to work the room, settling in on a beefy guy in a skullcap who carried himself like a man who’d done time. After a brief chat, Bolufé raced back.
“He’s a Cuban from Miami!” he whispered.
“Also deported?” I asked.
“He doesn’t want to tell me yet.”
* * *
In another part of town, inside a tall tower and behind four locked doors, is a local non-governmental organization called the Committee for the Defense of Human Rights. Its president, Billy Navarrete, listened intently as I described Bolufé’s situation. The organization is concerned about what it means to have so many foreigners dumped into a city that’s become a battleground for drug cartels that could prey on desperate migrants.
“Inevitably, they’ll need to talk to these gangsters,” he said. “It’s a reality. They have control.”
He assessed Bolufé’s situation. A Cuban passport wouldn’t get Bolufé far, because it bore no stamp of him having entered the country — thanks to the inaccurate paperwork Bolufé was handed on his way in.
“It’s something extraordinary,” Navarrete said.
Billy Navarrete, president of the Committee for the Defense of Human Rights in Ecuador, says his organization is concerned about so many foreigners dumped into a city that’s become a battleground for drug cartels.
Jose Pagliery/NOTUS
Navarrete thought about what to do. With some work — and some pleading with Ecuadorian officials — CDH might be able to get Bolufé’s documents in order so he can legally be present in Ecuador. Until then, it’s dangerous for him to go anywhere without an ID. Bolufé is once again an undocumented immigrant. I told Navarrete how Bolufé, in a fit of inspiration, had concocted an idea to put together a reggaeton event in neighboring Peru later in the summer. Navarrete winced.
“His asylum application in the United States could prove useful here,” he said. “But it’s a long process. It takes years.”
Part of the challenge is political.Maria Teresa Urueña, a program manager with the nonpartisan Center for Engagement and Advocacy in the Americas, told me that Bolufé faces an Ecuador that is less tolerant of people like him — regardless of the diplomatic deals that make these third-country removals possible.
“The deportation machine has become more sophisticated … and what protection can Ecuador really provide these asylees? Ecuador is not a safe place,” she said, noting that even she canceled a trip to Guayaquil in April due to security concerns.
Mobile Pathways, a tech nonprofit that monitors third-country expulsions and tracks court data, counted 1,836 Cubans who have been deported to Ecuador since the start of the second Trump administration. According to the Ecuadorian government, 94 of them have arrived in Ecuador seeking asylum in 2026 so far.
But there’s a flood on the horizon. According to Third Country Deportation Watch, a joint project between two humanitarian organizations, the U.S. immigration system in just the six months between December and June has put 18,552 people who aren’t from Ecuador on track for a forced deportation there. Among them are thousands of Venezuelans, Cubans, Guatemalans — and even a few from Burkina Faso, a West African nation that has become a battleground for Islamist militias.
Those who track U.S. deportation flights say they now arrive at Guayaquil airport three times a week on Mondays, Thursdays and Fridays.
* * *
During my layover in Miami on the way to Ecuador, I was reminded of the alternate reality on display in conservative Little Havana. The old Cuban-American guard hanging out at outdoor coffee shops still have MAGA bumper stickers on their pickup trucks. The fear of mistreatment and expulsion is only real for recent newcomers like Jose Reina, who fled Cuba and trekked across most of Central America with the help of a $5,000 human trafficking coyote. He managed to get legal status at the tail end of the Biden administration and now works at a hotel in South Florida.
“Those people that defend Trump are blind — no different than the communists I knew on the island,” he said. “One has to think for themselves.”
That’s not a parallel you hear often in Cuban-American circles, where politics have been dominated by Republican Party identity for decades. But it’s different for those at the receiving end of the Trump administration’s deportation apparatus.
Sitting on a lawn chair on a hill overlooking the dark waters of the Guayas River, Bolufé admitted that he admired Trump’s nostalgic vision of America and cheered him on — until he saw what ICE agents and private contractor prison guards were willing to do to detainees like him.
“You might think I’m crazy, and maybe I am. But I learned a lot being stuck in those ICE prisons, watching people suffering mental problems — the way due process and the Constitution is violated, the way people violate basic human rights, the way DHS and ICE lie unscrupulously,” he said.
Then he offered up something unexpected: Despite the chains and cold rooms and moldy shower stalls, he would have preferred to remain in ICE detention than get sent to Ecuador. At least it was a labyrinth he had learned to navigate.
“When they took away my papers, that actually hurt,” Bolufé said. “How are you going to take away the pitiful little proof I have against you? How can you fight for your rights against the very power that’s imprisoning you?”
Jose Pagliery/NOTUS
Thattraumatic past was starting to fade into the background. All the jail transfers and quarrels with guards were blurring together, Bolufé said, and he’d rather look forward than wrestle memories. My days with him in Guayaquil had jolted his inner drive, long suppressed from “everything that happened.” He pulled up his phone and tapped the screen. He began recording a voice note asking a reggaeton agent if his client would be interested in booking a show. When his cigarettes ran out, he treated himself to my pack and pulled out another. He turned his back to the river and called a buddy in Miami who asked what he was doing next.
He had big plans. He was headed to Mexico, he said — although he was unsure how he’d travel there. Or maybe Spain, where he’d claim some sort of asylum. Or maybe Egypt, to embrace his new faith, although he didn’t know a lick of Arabic. Well, he knew one word.
“Inshallah,” he said, with a wave of his left hand.
This story has been updated to correct the spelling of Billy Navarrete’s last name.
I took a vacation for the first two weeks of July and spent 14 day enjoying the small harbor towns around the Puget Sound, dinners with my extended family, the annual 4th of July holiday at the family house where after the sun sets we can watch everyone’s fireworks around the harbor.
Mary Trump Says Donald Trump Is a Weak Man in Decline — and Reveals Why She Refuses to Fear Him
Trump’s niece says the president is facing a “perfect storm” of cognitive decline, physical deterioration and lifelong psychological problems while revealing why she refuses to fear him
Press enter or click to view image in full size
Mary Trump, niece of President Donald Trump, recently sat down with former CNN anchor Jennifer Yellin for a long, in-depth conversation about her uncle’s mental and physical deterioration, the future of the MAGA cult and why she is not scared of him.
Mary Trump Says Her Uncle’s Body Is Falling Apart
Yellin and Mary Trump talked about Donald Trump’s recent trip to NATO, where he again showed signs of physical deterioration. Trump fell asleep during a meeting and often has a hard time staying awake in public.
According to Mary Trump, we are facing a perfect storm of problems with her uncle. She has long said that he has undiagnosed mental health issues. Now, those issues are combined with the stress of being president while his body is literally falling apart.
Again, Mary Trump pointed out that her grandfather — Donald Trump’s father, Fred Trump —…
Could not extract this article automatically (410 Client Error: Gone for url: https://levelup.gitconnected.com/a-better-model-wont-fix-your-ai-agent-a-better-harness-will-01976aaf51ee?gi=8976d3e87e32).
Read it on the original site instead.
In 1780, running a steam engine didn’t just require coal. It required a teenager.
They were called “valve boys.”
These boys pulled heavy iron levers on a high-stakes three-second loop to keep the boilers from stalling or exploding
Naturally, these boys believed they were irreplaceable. They mistook constant anxiety for irreplaceable value.
Then, in 1788, James Watt invented the Centrifugal Governor.
It was a simple, elegant device with two spinning metal balls.
When the engine ran too fast, centrifugal force flung the balls outward, automatically closing the steam valve. When it slowed, gravity pulled them back down, opening it.
Overnight, the job of valve boys was annihilated.
Watt made their entire labor philosophy look like a joke.
Look in the mirror. Look at your glowing screen and your tired eyes.
You type 500-word paragraphs begging for a frontend component, then sit there manually correcting the mistakes over and over.
Matt Murray wants to know if he should still take pictures of his rental car before he drives it off the lot. The answer is complicated.
The question came up when he rented an Audi A4 in Tampa recently.
“The car rental company had at least four cameras recording as the car left the gate,” remembers Murray, a real estate agent from Westport, Conn.
He told the attendant he preferred to take his own photos to document any possible damage, but an attendant assured him he didn’t need to. (He took pictures anyway.)
“But that got me thinking,” he says. “Do I still need to take pictures?”
Short answer: Yes, you do. And there’s a reason for it.
Why people take pictures of their rental car
Travelers take snapshots of their rental car for all kinds of reasons — to document their vacation, or to show off a hot set of wheels to their friends. But the biggest one is to show the condition of the vehicle before and after they rent it. They do it because they don’t want to get charged for damage to the vehicle that didn’t happen during their rental.
Before smartphones, taking images of a rental wasn’t always possible because you needed to travel with a camera. Now, many…
The mom who chooses not to vaccinate her child believes she is making the best possible decision for her child. She is not careless, and she is not stupid. She is doing what every parent does: trying to protect her kid with the information she has. Our goal is never to tell her she is wrong. It is to understand how she got there, and to reach her with the best available evidence in a way she can hear. The premise underneath all of this is that the person in front of you is reasonable and acting out of care.
Which is why, when it comes to comms, canned messages do not work.
I have been doing this in the public eye via social media for about six years now, and in that time, I have encountered a lot of characters, a lot of questions, and a lot of pushback. What I love about our platform is that we work painstakingly hard to create a place where all folks can come and be heard. This includes skeptics, the uncertain, and the simply curious, who have questions about vaccines, antibiotics, fluoride, or whatever it happens to be that day. While we love our loyal readers who understand the value of what we do, we’re not interested in only speaking to people who get it. That feels like preaching to the choir, and that’s not where the work is.
So instead, we spend most of our time behind the scenes of social media, constantly listening, conducting surveys, doing interviews, and running sentiment and thematic qualitative analyses of our messages, comments, and DMs, all in service of understanding the patterns beneath what people are actually saying.
A huge focus for us (obviously) is vaccines.
One thing to say upfront: most people are not vaccine-hesitant. The large majority still vaccinate themselves and their kids. This is not a piece about a country that has turned against vaccines, because it has not. It is about the smaller set of conversations that are harder, and how to have them better.
We are not the first to map this terrain. Researchers like Matthew Hornsey and Kelly Fielding have identified the “attitude roots” underlying vaccine hesitancy, and Stephan Lewandowsky’s group has gone further by identifying distinct profiles of hesitancy. Where that research maps the psychology, often in controlled European samples, our aim is to translate it for the US in its current moment, built from what we hear from real people every day, and simple enough to use in the first thirty seconds of a real conversation, whether you are a pharmacist or someone’s sister.
This framework is aimed at anyone having these conversations: clinicians, pharmacists, and science communicators, but also parents, family members, and anyone who has found themselves at a dinner table (or a bar!) trying to explain why they chose to vaccinate. And if you find yourself recognizing your own thinking in one of these personas, that is useful too. Understanding where your own hesitancy or uncertainty comes from is the first step toward working through it. For each persona, we have tried to distill not just who they are, but also what tends to work in conversation. Here is where we have landed so far.
The seven personas
1. The Naturalist. You probably know a Naturalist. They may say things like, “My body doesn’t need vaccines, I prefer natural protection.” They see “clean” living and “food is medicine” as real prevention, and view vaccines as meddling with a system that is already working. The Naturalist also frequently has concerns about ingredients, additives, and what is “really” in the shot, and often turns first to herbal remedies, supplements, or integrative medicine traditions before reaching for anything pharmaceutical. Listen for words like “natural” and “clean,” and frequent questions about ingredients. What works: engage the worldview rather than dismiss it. Reframe vaccines as something that supports the immune system rather than overrides it, and answer ingredient questions honestly and directly.
2. The Worried Well. This group is not outright opposed to vaccines, but may be anxious about side effects, feeling sick after vaccines, or getting “too many at once.” This is often a parent, and someone who wants to do the right thing, but is paralyzed by the fear of doing the wrong thing. Listen for questions about side effects, fevers, and dose spacing. What works: validate the worry, then reframe post-vaccine symptoms as signs the immune system is working, not signs of illness. Naming what to expect in concrete terms takes the fear out of the unknown.
3. The Skeptic. The Skeptic does not necessarily doubt the science. The doubt is aimed at the institutions delivering it (the CDC, pharma, and federal guidance) and at the motives behind the recommendation. They tend to assume a profit motive is driving it. To be clear, pharma is for-profit, and pretending otherwise does not build trust. The Skeptic’s wariness of institutions is not always misplaced, and that is part of why the conversation requires care rather than dismissal. Listen for mentions of CDC, pharma, “follow the money,” and “profit.” What works: transfer trust from the institution to the relationship in front of them, whether that is their pharmacist, their clinician, or someone in their community who knows them.
4. The Conspiratorial. This group is certain that vaccines are poison, a scam, or that someone is hiding something. This is the persona most associated with escalation risk in clinical settings, and the one where rational conversations or scientific evidence are often dismissed. Listen for “poison,” “scam,” “they’re hiding it,” and language that signals the conversation is not happening in good faith. What works: stay respectful, focus on building trust, and leave the door open for a future conversation rather than burning the bridge with a debate that will not change the outcome. You never know what seeds you may plant, even if it doesn’t seem like you got through. (A note on this term. We went back and forth on it, because the last thing we want is for someone who recognizes themselves here to feel dismissed, which defeats the whole point of this piece. We kept “Conspiratorial” not for the content of the belief, since plenty of personas hold mistaken ones, but for its shape: it assumes deliberate, hidden harm. We use the word with care and without contempt.)
5. The Confused. These people are victims of all the noise, and they are looking for a clear answer. There is no emotion behind their confusion; they are genuinely unsure given shifting guidance and schedule changes. In 2026, this might be the fastest-growing segment, and frankly, who can blame them? Listen for “wait, do I still need this?” questions about the current schedule, and questions about what changed. What works: Be the trusted source. Give a clear, direct answer about where things stand right now, without overloading them with the history of every guideline change that got us here.
6. The Unimpressed. As far as the Unimpressed is concerned, the experiment has already been run. They, or someone close to them, got vaccinated and “still got sick”; they “already had the disease” and figure they don’t need the shot; or they simply heard the vaccine “doesn’t really work” (the flu shot is the usual example). Either way, the verdict is the same: what’s the point? The Unimpressed needs a reframe of what “working” actually means, since reducing severity is not the same as preventing every infection. Listen for “still got sick,” “already had it,” “doesn’t work,” and “what’s the point.” What works: reset the goalposts. Vaccines are not a force field; they are a way to make the version of illness you do get less severe, shorter, and less likely to result in hospitalization or death.
7. The Deferrer. The Deferrer is not opposed to vaccines. They just have not gotten around to it yet, held up by friction like time, cost, coverage, or wanting to check with their doctor first. The Deferrer is often the lowest-hanging fruit, and we miss them constantly because we assume they are a no when they are actually a not-yet. Listen for “not today,” “my doctor,” “cost,” and “next time.” What works: remove the friction, whatever friction you can remove. For a clinician or pharmacist, that means walk-ins, no appointment, and a coverage check on the spot: “We can do it right now while you are here.” For everyone else, it is making the path obvious: tell them it is walk-in and covered, send the booking link, or offer to go together. The move is the same either way; take the next step off their plate.
This is our 1.0 version of the persona list, and we have been thinking about it for a very long time.
A few things to keep in mind
These personas are not clean buckets, and most real people are a blend. A common pattern right now is a Confused patient with a Skeptic streak who says something like, “I don’t know what to listen to anymore, and honestly, I’m not sure I trust the people changing the rules.” Someone can be a Worried Well parent who is also a bit of a Naturalist, or a Deferrer who tips into Unimpressed after their third bout of COVID. The personas are a starting point for the conversation, not a label to slap on someone.
It is also important to be realistic about what success looks like. We are not trying to “win” conversations or shift anyone’s entire worldview in a single conversation. Success is often partial, and that’s ok. Someone can swear by herbal supplements and still roll up their sleeve. I’d argue that a Naturalist who decides to vaccinate while still buying her elderberry gummies is a win.
A lot of people in this space talk about the “movable middle,” meaning the segment of folks who can actually be reached with the right conversation. I would not try to map these personas cleanly onto that idea, but my honest read is that some are more reachable than others. The Confused, the Deferrer, the Worried Well, and even a lot of Naturalists are very much in the movable middle. The Conspiratorial persona, generally, is not, and a clinician or pharmacist trying to win that conversation in a five-minute window is going to burn time that would be better spent with the patient behind them in line. That is not a judgment of the patient; it is just an honest accounting of where a clinician’s limited bandwidth is best spent.
This works for more than vaccines
This framework can be applied to almost any health topic where there is tension between the evidence and the public conversation, whether that is fluoride, seed oils, sunscreen, GLP-1s, or microplastics. We all see the world through our own lenses, we consume information differently, and we place emphasis on different priorities.
So why would I, or a clinician or pharmacist with an extremely limited window of time with patients, spend that time talking to a Goop/Gwyneth/crunchy type about the extreme rigor of pharma regulation if her hesitancy lies in the belief that infection immunity is superior? While we are on the subject, can we retire “natural immunity”? My team has been pushing for “infection immunity” or “survivor immunity” because you have to first survive infection to get the good stuff.
One evidence base. Many topics. And for each topic, seven different conversations.
Why we think this approach works
A reasonable question at this point is whether any of this works. We have not yet formally tested the seven-persona framework against patient outcomes, and that work is in progress. But we have a fair amount of evidence that the underlying listen-first, meet-people-where-they-are approach is doing something.
Our 2026 audience survey of more than 1,500 respondents found that 65% use our content to make health-related decisions, and more than half share it with family and friends. We also see actual behavior change: On Instagram, 17% of followers say our content significantly changed their perspective, and 34% stopped following a harmful health trend after seeing our content. On Facebook, 15% of followers took a vaccine-related action after engaging with our content. None of this is a controlled trial, but real behavior change is rare in this space, and these numbers are a positive indication.
More specifically, within the persona framework, we have built virtual escape rooms that use these personas to train clinicians to navigate real conversations with hesitant patients. After completing the training, clinicians report gaining substantial clinical confidence on tough topics, including a 67-point jump in confidence when discussing vaccine ingredients (the kind of question a Naturalist tends to bring) and a 44-point jump in confidence when addressing medical mistrust (the kind of question a Skeptic tends to bring). Whether that confidence translates into more effective conversations with actual patients is the next thing we need to study, and we are working on it.
A caveat about scale
In a one-on-one conversation (a pharmacist and a patient, a clinician and a parent, you and your hesitant brother-in-law at Thanksgiving), tailoring to a specific persona is realistic and probably essential. In a mass campaign in communities or on social media, we cannot tailor to one person, because audiences are mixed and exposed to everything at once.
What this means for mass communication is slightly different. Instead of tailoring a single message, we put out a varied portfolio of messages that address different personas and concerns over time. One post addresses ingredient questions, another addresses shifting federal guidance, and another addresses the “I already had it” reframe. Over weeks and months, the audience gets exposed to the message that speaks to where they actually are, even if we never knew which persona they belonged to.
What is next?
This is a work in progress. We are still testing and refining these personas against new pulse survey data, feedback from clinicians and pharmacists on the front lines, and our ongoing listening across social media. The labels will probably shift, the triage cues will sharpen, and there may be more (or fewer) than seven. I wanted to give you a sneak peek at where our thinking is right now, and at the approach we keep coming back to, because the alternative (which is to keep blasting canned “vaccines save lives” messages at audiences who have already tuned them out) is not working for anyone. If something here resonates, or if you read a persona and thought “you’re missing one,” we would love to hear it, because that is exactly the kind of input that gets us to a 2.0.
If you’d like to support this work, please consider a tax-deductible donation to The Center for Unbiased Science and Health (CUSH), our nonprofit that funds educational initiatives like this one.
In recent years, betting has evolved into a global digital industry, raising significant public health concerns. The widespread availability of online platforms and mobile access has made gambling more accessible than ever, effectively creating what experts describe as a “casino in your pocket” (WHO, 2024).
Among the various forms of gambling, slot machines, particularly digital ones, are considered especially risky. Their rapid gameplay, continuous reinforcement, and instant rewards are strongly linked to addictive behaviors, which makes them one of the main drivers of problem gambling worldwide (Techopedia, 2024).
The scale of the issue is significant. Global estimates suggest that around 1.2% of adults suffer from gambling disorder, while many others experience financial, psychological, and social harm due to gambling-related activities (WHO, 2024).
At its core, the question is whether betting can generate consistent long-term returns. The most rigorous way to answer this is through mathematics.
Casino games are designed around an inherent statistical advantage known as the house edge, which ensures that, over time, expected returns are negative for the player.
While short-term wins are possible and often emphasized, probability theory makes it clear that repeated play leads to predictable losses rather than sustainable gains.
For those familiar with my previous articles, I am an economist and data scientist working in financial risk, and I enjoy raising thought-provoking questions for discussion.
In this article, we explore the key mathematical principles that demonstrate why consistent long-term profits from betting are unlikely. To make this concrete, we also build a simple online casino using Python and Streamlit.
We do this by:
Explaining the math and statistics behind long-term returns in digital slot machines.
Coding a basic casino model in Python and running the game in Streamlit, which is engaging to play.
Running simulations for multiple players on a single slot machine.
Article Structure
This article has three sections:
Maths and equations.
Python functions to run the game.
Streamlit Integration.
The first section presents mathematical theory behind online betting, especially in casino systems.
The second demonstrates how to build a simple slot machine for experimental purposes, covering the core backend logic that powers the game.
The third focuses on a basic frontend implementation to render the application for single-player and multiplayer modes, where I demonstrate some insights about betting in the short and long term.
Symbols and Weights
In a slot game, symbols represent the outcome of a single spin. Each time the slot is played, every symbol has a probability of appearing on the screen.
This probability is determined by a set of predefined weights, in other words, how rare each symbol is relative to the others.
To make this clear, consider a machine with seven different symbols, ordered by rarity: tiger, gold, clover, star, seven, wild, and jackpot.
Also, let’s assume default weights are given as follows:
The initial weight configuration is heavily skewed toward low-tier symbols, with tiger, gold, clover, and star all assigned equal and dominant weights (5), meaning they will appear very frequently and define most of the player experience.
In contrast, higher-value symbols like seven (1), jackpot (0.5), and especially wild (0.3) are significantly rarer, which is a typical design choice to preserve excitement and control payouts.
Think of weights as “tickets in a draw.” The more tickets a symbol has, the more often it shows up.
Instead of working with probabilities for each symbol, we normalize the weights to get them, Equation 1:
Equation 1. Probability of Symbol i
pi is the probability of symbol i appearing;
wi is the initial weight assigned to symbol i (controls how frequent it is);
N is the number of initial symbols (5 in this case) ;
J is the index used to iterate over all symbols;
And the term in the summation represents the total weight across all symbols, acting as the normalization factor.
Intuitively, you take the weight of one symbol and divide it by the total weight of all symbols to get its probability.
We could start directly with probabilities, some would need to be extremely small, making them difficult to discuss.
Let’s take the tiger and jackpot as examples (see Equation 2):
Equation 2.Probability of a Symbol Combination
The probability of getting one tiger is 18.66% while the probability of one jackpot is 1.87%, way rarer.
Winning Positions
As the saying goes:
As the saying goes: One swallow does not make a summer. Nor does a tiger alone.
This brings up the concept of winning positions.
A winning position is a rule that establishes how many matching symbols must appear together, and in which positions, for a prize to be paid.
Usually, this number is three for common slot machines, but it can be set to any value. In a game based on a 1 × 3 matrix, the columns indicate the positions where a symbol can appear. A win occurs when three matching symbols are displayed on the screen simultaneously.
Since all 3 symbols have the same probability of appearing once, we come up with Equation 3:
Equation 3.Probability of a Winning Line
For this particular case a jackpot winning position happens approximately once in 153,846 chances.
Why is the probability of a wild lower than that of a jackpot?
In this game, the wild symbol acts as a substitute, meaning it can complete a winning position when combined with a matching pair of another symbol. Because of this flexibility, it contributes to many more winning combinations than a regular symbol.
To keep the game balanced, the wild must appear less frequently than the jackpot. Even though it shows up less often, when it does appear, it significantly increases the chances of forming a winning position.
Since we have defined a winning position in this 1 x 3 matrix game, adding more lines increases the overall chance of winning. Each line is independent, meaning the outcome of one line does not affect another.
However, these events arenot mutually exclusive, since multiple lines can win at the same time. Because of that, we cannot simply sum their probabilities.
Instead, the correct approach is to compute the probability of winning in at least one line using the complement rule, by calculating the probability that all three lines lose simultaneously and subtracting it from one.
Therefore, the probability of getting at least one winning line with a 3 x 3 matrix game is given by Equation 4:
Equation 4. Probability of at Least One Winning Line
Where i represents any symbol, and L is the number of winning lines — here, 3 horizontal lines. The exponent 3 comes from Equation 3, since a winning line requires three identical symbols. The term inside the brackets is the probability of losing on a single line.
Now that we understand the classic example, let’s add some more fun to the game. Considering only the chance to win horizontally is not ideal in terms of user experience and gameplay. We need to add more fun so that the slot machine becomes more attractive to players.
One way to do so is to define a couple of additional winning positions. For now, we are focused on horizontal lines, but we could also consider moving vertically, in terms of columns, as well as diagonals, and maybe some left and right triangles to keep the mood high.
Here is a representation of all possible winning lines in a 3 × 3 game, Figure 2.
Press enter or click to view image in full size
Figure 2. Winning Positions in a 3 × 3 Game
We see that there are 4 possible winning positions, represented by rectangles: V (vertical), H (horizontal), D (diagonals), and > (left triangle).
Each V can occur 3 times, since there are 3 columns; the same goes for H, with 3 horizontal lines. In a matrix, there are only 2 possible diagonals, and the triangle can occur on the left side, as depicted above, but also on the right side.
Thus, summing 3V + 3H + 2D + 2> gives 10 winning positions in a single spin.
Wherever the odds are for each symbol to appear, there are 10 possible configurations that will pay the prize.
From that we define another hyperparameter and can update the initialization of the game:
Previously, we defined how to win in the game, but how much a win pays is yet to be set.
Since each symbol has a certain rarity of being displayed on the screen, it is intuitive to assume that the rarer the symbol, the higher the payout.
So, taking tiger, gold, clover, star, and seven as examples, we could assume that the star pays more than all of them. However, since tiger, gold, and clover are equally frequent in the game, they should pay the same, right?
That makes sense, but once again, our game would become boring. Therefore, we must configure a hyperparameter that determines the payout for each symbol in a winning position, while still encouraging the player to root for the higher-paying symbol, even when the odds are the same.
This is achieved by setting default multipliers as follows:
Note now that star is as well as frequent as tiger but pays additional 6 units in return. Seven is 5 times rarer than the precedent ones, and pays additional 13 units when compared with tiger.
Why do wild and jackpot pay nothing back?
As mentioned above, the wild works as a substitute, so on its own it pays nothing. We must ensure that whenever a winning position can be formed using a wild, the highest possible prize is paid.
That said, in a case where both a tiger winning position and a seven winning position could be formed using a single wild, we assume it will form the higher-paying combination, and in this case, bind it to that outcome.
The jackpot has a similar behavior: it pays nothing directly because, when it appears in a winning position, the player receives the accumulated prize, which is funded by a fraction of each bet.
In this case, the rarer the odds of a winning position, the higher the total prize paid out.
We covered that each symbols pays back differently, but they always should pay back more than the overall bet for a winning position.
We define 10 as the number of winning positions, so when we bet, let’s say, $10 per spin, we are dividing the value of bet for the number of 10 possible winning positions.
It means that for each $10 bet, we are allocating $1 to a winning position that, when displayed on the screen, is multiplied by the default multiplier.
Thus, if we win with tiger, we get $12 back; if we win with clover, we get $16 back; and of course, if we win with both, the total payout is $28 from a $10 bet.
In terms of the risk–reward ratio, we have 1:2.6, for each dollar we bet, we expect to receive $2.6 back.
Note that default multipliers are hyperparameters, and each should be set such that the multiplier times 10% of the total bet exceeds the original bet. This is a default constraint for the total number of winning positions we have.
To make this clear, tiger pays $12 for each $1 when the total bet is $10. Therefore, when a single tiger wins, the minimum payout is $12 × $1, which must be greater than — and indeed is greater than — $10.
We covered that each symbols pays back differently, but they always should pay back more than the overall bet for a winning position.
We define 10 as the number of winning positions, so when we bet, let’s say, $10 per spin, we are dividing the value of bet for the number of 10 possible winning positions.
It means that for each $10 bet, we are allocating $1 to a winning position that, when displayed on the screen, is multiplied by the default multiplier.
Thus, if we win with tiger, we get $12 back; if we win with clover, we get $16 back; and of course, if we win with both, the total payout is $28 from a $10 bet.
In terms of the risk–reward ratio, we have 1:2.6, for each dollar we bet, we expect to receive $2.6 back.
Note that default multipliers are hyperparameters, and each should be set such that the multiplier times 10% of the total bet exceeds the original bet. This is a default constraint for the total number of winning positions we have.
To make this clear, tiger pays $12 for each $1 when the total bet is $10. Therefore, when a single tiger wins, the minimum payout is $12 × $1, which must be greater than — and indeed is greater than — $10.
Since we intuitively understood the dynamics of the payouts, let’s formalize the expected payout for any winning lines in Equation 5:
Equation 5. Expected Payout per Winning Line
Where:
E[payout] is the expected total payout;
B is the total bet per spin (e.g., $10);
W is the total number of possible winning positions (e.g., 10);
k is the number of winning positions observed in the experiment (e.g., 1);
mi is the multiplier paid when symbol i forms a winning position.
Now, as an example, for one winning position formed by three tigers, with a total bet of $10 and 10 possible winning positions, we obtain an expected payout of $12 (see Equation 6).
Equation 6. Expected Payout for a Tiger Winning Position
RTP (Return to Player)
We have covered symbols and their weights, winning positions, and expected payouts — all the elements needed to estimate the Return to Player (RTP) in the long run.
For any given spin, there are only two possible outcomes: either the player loses the entire bet or wins an amount greater than the original stake.
Since each symbol has an associated probability (pi) and we have defined all possible winning positions (wp), RTP represents the average amount of money returned to the player over many spins.
To compute the Return to Player (RTP), we take the expected payout over all possible winning outcomes.
In this setup, each winning position receives 10% of the total bet, and a win requires three identical symbols. Therefore, the probability of a winning combination for symbol i is pi³ (see Equation 1).
Each symbol also has an associated multiplier mi, which defines its payout. By combining these elements, the RTP simplifies to:
Equation 7. RTP (Return to Player)
This means the RTP is simply the sum, over all symbols, of the probability of hitting three of that symbol multiplied by its respective payout multiplier.
In other words, RTP represents the average return per unit bet in the long run.
There are three possible outcomes for the RTP in a slot machine game:
If it is greater than 1, the player wins more than they bet.
If it is equal to 1, the player breaks even over time — a scenario that offers little incentive to play.
If it is less than 1, the player loses more money than they win in the long run.
Yes, it does. Since the RTP is set slightly below 1, the game is designed to make players feel like they are winning — because each individual win pays more than the original bet. However, wins are rare enough that the total return remains below 1 over time.
Remember those hyperparameters from earlier? They are carefully calibrated to create the feeling of “I might win today.” In the short run, it is entirely possible to achieve high returns due to randomness, driven by the symbol weights. However, over time, the player typically gives back all winnings and eventually loses the initial stake.
Furthermore, some games can be configured to have an RTP greater than 1 for a limited number of spins, and then revert to below 1 in order to recover the house’s expected profit.
In practice, let us assume 5 spins, each with a $10 bet, where each spin either returns a payout or 0 (see Equation 7). This empirically simulates the process.
For these 5 spins, the payouts are 12, 0, 16, 0, and 20, summing to $48 returned out of $50 wagered. The resulting RTP in this case is 0.96, which is quite close to what slot machines are typically calibrated to achieve.
Equation 8. Empirical RTP from Five Spins
For 5 spins, the payouts are 12, 0, 16, 0, and 20, summing to $48 returned out of $50 wagered. The resulting RTP is 0.96, which is quite close to what slot machines are typically calibrated to achieve.
The winning rate in this case is 3 out of 5, meaning the player wins 60% of the spins, yet still loses $2 over those 5 spins. In the long run, if this RTP remains constant, after more 44 spins the player would be expected to lose the entire initial amount wagered.
So, Good luck!
Coding a Slot Machine in Python and Streamlit
Based on what we’ve built so far, here’s a clear way to guide the reader through the code:
We’ve already covered the core logic behind a slot machine game. Now, it’s time to translate those concepts into a working web application using Python.
Some parts of the code reuse ideas explained in earlier sections, so they’ll be referenced briefly to avoid unnecessary repetition.
This section is organized into the following topics:
Required libraries.
Initial setup and parameters.
Game functions.
Streamlit interface.
Required Libraries
Below is the complete list of libraries used in this project, organized by their role in the application:
import streamlit as st
import pandas as pd import numpy as np
import plotly.express as px import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation, PillowWriter import plotly.graph_objects as go
from scipy.optimize import minimize
import random import time import hashlib
from typing import List, Tuple from collections import Counter
Streamlit powers the web interface, handling user input, layout, and real-time interaction.
Pandas and NumPy support data handling and numerical operations, making it easy to manage game state and calculations.
Plotly and Matplotlib are used for visualization, Plotly for interactive charts within the app, and Matplotlib for more controlled or animated graphics. SciPy provides optimization tools, useful for tuning parameters and simulations.
Finally, standard libraries like random (game randomness), time (timing control), and hashlib (deterministic hashing) support core game mechanics.
Initial Setup and Parameters
We start by defining the symbol system, which represents the core of the slot machine. Each symbol has a name, a visual emoji representation, a payout multiplier, and a probability weight that controls how often it appears.
SYMBOLS: defines all possible outcomes in the slot machine, representing the full set of symbols that can appear during a roll.
SYMBOL_EMOJI: maps each symbol to its visual representation, used to render the interface in a more intuitive and engaging way.
DEFAULT_MULTIPLIERS: controls the payout value associated with each symbol, directly impacting reward calculation when winning combinations occur.
DEFAULT_WEIGHTS: defines the probability distribution for each symbol, determining how frequently each outcome appears during random selection.
INITIAL_BALANCE: sets the starting amount available to the player before any bets are placed or rounds are played.
WINNING_POSITIONS: defines how many positions are evaluated in the game grid when checking for winning combinations.
These parameters act as the foundation of the game, ensuring consistency across each run.
The Wild and Jackpot symbols are special cases: they are reserved to modify gameplay dynamics. The Wild increases the chances of forming winning combinations, while the Jackpot is tied to the highest reward, representing the grand prize for a lucky outcome.
I previously configured the game to make it fun and balanced to play. This initial configuration can be adjusted by the game owner, depending on the desired experience or difficulty.
In the next sections, I will show a step-by-step approach to optimize these parameters for a target RTP (Return to Player). For now, we will assume they represent a fair starting point.
Game Functions
Now, let’s move on to the core functions of the application. We gradually increase in complexity as we proceed.
The first function generate_seed() creates a random value using random.random() and converts it into a string. It simply returns a random number as text.
The second function hash_seed() takes a seed, encodes it with seed.encode(), and then applies hashlib.sha256() to generate a fixed-length hash. The result is returned as a hexadecimal string using .hexdigest().
spin() simulates a random "spin" using a controlled seed. It creates a deterministic random generator with random.Random(seed).
It then uses rng.choices() to select 9 items from SYMBOLS, following the probability distribution defined by weights.
Get Guilherme Ziegler’s stories in your inbox
Join Medium for free to get updates from this writer.
The result is a list of 9 randomly chosen symbols, in other words, the output of a user interaction with the slot machine.
def get_positions(grid: List[List[str]]) -> List[List[str]]: positions = [] positions.extend(grid) for col in range(3): positions.append([grid[row][col] for row in range(3)]) positions.append([grid[i][i] for i in range(3)]) positions.append([grid[i][2-i] for i in range(3)]) positions.append([grid[0][0], grid[1][1], grid[2][0]]) positions.append([grid[0][2], grid[1][1], grid[2][2]]) return positions
get_positions() is built to extract all possible winning patterns from a 3x3 grid in a single spin. We can classify these patterns into four groups:
Vertical lines (V): 3 columns → each column forms one possible line.
Horizontal lines (H): 3 rows → each row is already included directly from the grid.
Diagonals (D): 2 main diagonals.
Triangles (>): 2 additional asymmetric patterns formed by selecting corners with the center.
So the function collects:
3 vertical positions by iterating over columns.
3 horizontal positions directly from the grid.
2 diagonal positions.
2 triangular patterns (left and right skewed combinations).
In total, that results in 10 possible winning positions per spin, represented as different ways of traversing the same 3x3 matrix.
for symbol in unique_symbols: count = non_wild.count(symbol) total_count = count + wild_count
if total_count == line_len: line_payout = bet_per_position * multipliers.get(symbol, 0) if line_payout > best_payout: best_payout = line_payout best_symbol = symbol
if best_payout > 0: payout += best_payout winning_positions.append(f"{best_symbol} line (with wild) = {best_payout:.2f}")
for line in positions: evaluate_line(line)
if show_winning_positions and winning_positions: st.write("Winning positions:") for wl in winning_positions: st.write(f" {wl}")
return payout
This function is responsible for calculating the total payout of a spin in a grid-based slot system. The grid is first reconstructed from the flat result list into a square matrix of dimension size × size, splitting the symbols into rows of equal length.
All possible winning patterns are extracted through get_positions(grid), which returns every predefined payline, including rows, columns, diagonals, and the additional custom winning configurations introduced earlier.
The total bet is evenly distributed across all available winning positions, producing the value bet_per_position used during the evaluation of each payline. Since a single spin contains ten possible winning positions, each position receives one tenth of the total wager.
Each payline is evaluated independently through the nested evaluate_line() function.
The highest-priority case occurs when an entire line is composed of “jackpot” symbols. In this situation, the accumulated value stored in st.session_state.jackpot is awarded to the player and immediately reset to zero, representing the highest possible payout in the game.
For regular evaluations, the function separates “wild” symbols from the remaining symbols while ignoring “jackpot” in the standard payout calculation. Lines containing only wilds or jackpot symbols are skipped because they do not define a valid paying combination.
When a line contains a single non-wild symbol together with optional wild symbols, the function verifies whether the wilds complete the entire line. If they do, the payout is computed using the corresponding value stored in the multipliers dictionary.
A second case occurs when a line contains exactly two distinct symbols together with one or more wilds. In this situation, both possible winning combinations are evaluated, and the function automatically selects the configuration producing the highest payout. This guarantees that the player always receives the most valuable valid combination.
After all paylines have been processed, the function optionally displays every winning position together with its corresponding payout whenever show_winning_positions is enabled.
The accumulated payout for the entire spin is returned.
play_round() combines all previous components into a single gameplay cycle, executing one complete spin from seed generation to payout calculation.
play_round() starts by generating a random seed using generate_seed(), then creates a secure representation of it with hash_seed(seed).
It then uses the seed to drive spin(seed, weights), producing the final result, which is the grid outcome based on weighted randomness.
It calculates the final payout using calculate_payout(), passing the result, bet, multipliers, and the fixed grid SIZE.
It returns:
The result (spin outcome).
The payout (total winnings).
The hashed seed (for verification).
And the original seed (for reproducibility).
For each spin that a user plays, the expected return for the player (RTP) is estimated by dividing the total_payout by the total_bet. However, RTP can also be estimated over repeated trials using the estimate_rtp function, which simulates multiple spins to approximate long-term return behavior.
def estimate_rtp(weights: List[float], multipliers: dict, n_samples: int = 20000, bet: float = 1.0) -> float: returns = [] for _ in range(n_samples): seed = generate_seed() result = spin(seed, weights) payout = calculate_payout(result, bet, multipliers, size=SIZE, show_winning_positions=False) returns.append(payout) return np.mean(returns) / bet
It simulates many rounds of the game, calculates the average payout from those simulations, and divides it by the bet value. This provides an approximation of the long-term return rate of the game.
Particularly, it is used to optimize weights and multipliers in the optimize_RTP function, shown below:
base_w = np.array(default_weights) if default_weights is not None else np.ones(n_symbols) base_m = np.array(list(default_multipliers.values())) if default_multipliers is not None else np.ones(n_symbols)
if not lock_weights: base_w = base_w / np.sum(base_w)
if best_err < 0.01: return best_w, best_m, best_rtp, history
if i % 50 == 0: step_size *= 0.9
return beam[0][2], beam[0][3], beam[0][1], history
It searches for an optimal combination of symbol weights and multipliers that best match a target RTP value.
It starts by initializing the system with default weights and multipliers, converting them into NumPy arrays. If weights are not locked, they are normalized so that their sum equals 1, ensuring a valid probability distribution across all symbols.
It identifies the special symbols “wild” and “jackpot” by their indices, since they can optionally be locked during optimization to preserve game balance constraints.
The process is driven by a simulated evaluation function: simulate(w, m) calls estimate_rtp using 500 simulated samples, returning an empirical RTP based on the current configuration of weights and multipliers.
To explore the solution space, a mutate function introduces controlled random variations into both weights and multipliers. Gaussian noise is applied, and values are clipped within predefined bounds.
If weights or multipliers are locked, they remain unchanged. Wild and jackpot symbols can also be explicitly frozen to prevent distortion of core game mechanics.
This allows controlled variation in the system without drifting away from the original target RTP. A key issue in this optimization is degeneracy: if both weights and multipliers are left fully flexible, the algorithm tends to collapse into a trivial solution.
In practice, it often converges by fixing one side of the system (for example, making all weights nearly uniform and compensating only through multipliers, or the opposite).
While this still satisfies the numerical RTP constraint, it undermines the structural integrity of the game design. The optimization effectively reduces the problem to a single adjustable axis, which removes meaningful interaction between symbol frequency and payout design.
To avoid this behavior, the system introduces controlled restrictions such as parameter locks and bounded perturbations. These constraints force the optimizer to distribute changes across both weights and multipliers instead of over-optimizing a single dimension.
Additionally, specific symbols like “wild” and “jackpot” can be locked independently to preserve core gameplay mechanics and prevent instability in critical reward paths. This is crucial to make the game more attractive to players and to control how many jackpots are paid by spins.
Each candidate configuration is evaluated using an error metric defined as the absolute difference between the simulated RTP and the target RTP.
The optimization itself follows a beam search strategy:
It maintains a population (beam) of the best configurations.
At each iteration, it generates multiple mutated candidates per beam element.
Candidates are ranked by error and only the best ones are kept.
Over time, the search gradually refines solutions, and the step_size is reduced every 50 iterations to stabilize convergence.
If a configuration achieves an error below 0.01, the function terminates early and returns the best weights, multipliers, achieved RTP, and full optimization history.
Finally, if convergence is not reached within the maximum number of iterations, the best configuration found in the beam is returned along with its performance history.
def plot_trajectory(all_runs, change_spin=None):
fig = go.Figure()
colors = px.colors.qualitative.Dark24
for i, run in enumerate(all_runs): fig.add_trace(go.Scatter( x=list(range(len(run))), y=run, mode='lines', name=f'Sim {i+1}', opacity=0.85 if i < 5 else 0.5, line=dict( width=2 if i < 5 else 1, color=colors[i % len(colors)] ) ))
if change_spin is not None: fig.add_vline( x=change_spin, line_dash="dash", line_color="red", annotation_text=f"Parameter change at spin {change_spin}", annotation_position="top right" )
This function visualizes how the player’s balance evolves over time across multiple simulations. It’s used to give a visual representation of multiplay simulations, in section four of this article.
It receives all_runs, where each run is a list representing balance changes over spins.
For each run, it adds a line plot using Plotly, where:
x represents the spin index
y represents the balance at each spin
Each simulation is drawn with a different color to allow comparison between trajectories.
If change_spin is provided, a vertical dashed red line is added to indicate the point where a parameter change happened in the game.
The result is a visual representation of volatility, risk, and long-term behavior of the game balance over time.
Streamlit Integration
Streamlit runs the script from top to bottom on every interaction, so we need session_state to persist variables across reruns.
Each object below is initialized only if it doesn’t already exist, and then Streamlit keeps it stored in memory during the session.
This setup defines the core state of the slot machine:
st.title("Slot Machine PRO - With Wild & Jackpot")
if "weights" not in st.session_state: st.session_state.weights = DEFAULT_WEIGHTS if "multipliers" not in st.session_state: st.session_state.multipliers = DEFAULT_MULTIPLIERS.copy() if "balance" not in st.session_state: st.session_state.balance = INITIAL_BALANCE if "history" not in st.session_state: st.session_state.history = [] if "jackpot" not in st.session_state: st.session_state.jackpot = 1000 if "jackpot_contribution" not in st.session_state: st.session_state.jackpot_contribution = 0.02 if "payouts_multi" not in st.session_state: st.session_state.payouts_multi = [] if "rtp_history" not in st.session_state: st.session_state.rtp_history = [] if "total_bet" not in st.session_state: st.session_state.total_bet = 0 if "total_payout" not in st.session_state: st.session_state.total_payout = 0streamlit integration
A quick discussion on the parameters not covered before:
balance tracks the player’s current money, which increases or decreases after each spin depending on the payout.
history stores game events over time, recording what happened in each spin.
jackpot stores the current jackpot pool; it starts at $1,000 and grows by a fraction of each bet placed.
jackpot_contribution defines how much each bet feeds the jackpot; in this case, 2% of the bet value is added per spin.
payouts_multi and rtp_history track performance metrics over time.
total_bet and total_payout accumulate the total amount wagered by the user and the total amount returned by the game.
Once initialized, these values are automatically preserved and updated through st.session_state, allowing the application to maintain state across reruns instead of resetting every interaction.
Next we divide our application in 3 distinct tabs:
tab_game, tab_sim, tab_opt, = st.tabs(["Game","RTP Optimization","Monte Carlo Simulation"])
used to create a better user experience.
In tab_game, we render the slot game animation, the current balance, the total jackpot amount, and the action buttons for single spin and multi-spin.
with tab_game: col_jackpot, col_balance = st.columns(2) with col_jackpot: st.metric("💎 JACKPOT", f"${st.session_state.jackpot:.2f}") with col_balance: st.metric("💰 BALANCE", f"${st.session_state.balance:.2f}")
grid_placeholders = [] for _ in range(3): cols = st.columns(3) row_placeholders = [] for col in cols: row_placeholders.append(col.empty()) grid_placeholders.append(row_placeholders)
col_buttons = st.columns(3) with col_buttons[0]: bet = st.number_input("Bet", 1, 500, 10) spin_clicked = st.button("Spin")
with col_buttons[1]: multi_spin_count = st.number_input( "Number of spins", min_value=2, max_value=1000, value=10, key="multi_spin_count" ) multi_spin_clicked = st.button("Multi Spin")
if spin_clicked and st.session_state.balance >= bet: jackpot_contribution = bet * st.session_state.jackpot_contribution st.session_state.jackpot += jackpot_contribution st.session_state.balance -= jackpot_contribution
grid_len = SIZE * SIZE for _ in range(15): fake = random.choices(SYMBOLS, k=grid_len) grid_fake = [fake[i:i+SIZE] for i in range(0, grid_len, SIZE)]
for i in range(SIZE): for j in range(SIZE): symbol = grid_fake[i][j] grid_placeholders[i][j].markdown( f"<h1 style='text-align: center;'>{SYMBOL_EMOJI[symbol]}</h1>", unsafe_allow_html=True )
time.sleep(delay) delay *= 1.1
grid_result = [result[i:i+SIZE] for i in range(0, grid_len, SIZE)]
for i in range(SIZE): for j in range(SIZE): symbol = grid_result[i][j] grid_placeholders[i][j].markdown( f"<h1 style='text-align: center;'>{SYMBOL_EMOJI[symbol]}</h1>", unsafe_allow_html=True )
if st.session_state.history: all_payouts = [h["payout"] for h in st.session_state.history]
col1, col2 = st.columns([2, 1])
with col1: st.write("Payout Distribution Histogram") chart_data = pd.Series([round(p, 1) for p in all_payouts]).value_counts().sort_index() st.bar_chart(chart_data)
with col2: st.write("Payout Statistics") st.write(f"Total spins: {len(all_payouts)}") st.write(f"Average payout: {round(np.mean(all_payouts), 2)}") st.write(f"Max payout: {round(np.max(all_payouts), 2)}") st.write(f"Min payout: {round(np.min(all_payouts), 2)}") st.write(f"Std dev: {round(np.std(all_payouts), 2)}") st.write(f"Zero payouts: {sum(1 for p in all_payouts if p == 0)}")
winning_spins = sum(1 for p in all_payouts if p > 0) st.write(f"Winning spins: {winning_spins} ({round(winning_spins/len(all_payouts)*100, 1)}%)")
if "weights" in st.session_state: del st.session_state.weights if "multipliers" in st.session_state: del st.session_state.multipliers if "all_runs" in st.session_state: del st.session_state.all_runs
st.rerun()
At the top of the interface, the current jackpot and player balance are displayed as live metrics. These values are continuously read from st.session_state, ensuring they reflect the true game state after every interaction.
Below that, a 3x3 grid of placeholders is created. This grid acts as the visual canvas for the slot machine, where each cell is dynamically updated during spins to simulate reel movement and final symbol placement.
The player can interact with the system by setting a bet value and choosing between a single Spin or a Multi Spin operation. These two modes share the same underlying game logic but differ in execution flow and performance cost.
When a single spin is triggered, a portion of the bet is first allocated to the jackpot via jackpot_contribution, and deducted from the player’s balance. The play_round() function then generates the outcome, including the symbol grid, payout, and cryptographic seed values.
Before revealing the final result, a short animation plays where random symbols are rapidly displayed in the grid, simulating reel motion. Once the animation ends, the final grid is rendered and the true result is revealed.
The player’s balance is then updated based on payout - bet, and all relevant state variables such as history, payouts_multi, total_bet, and total_payout are updated. The system also recalculates and stores the cumulative RTP in rtp_history, allowing long-term performance tracking.
The multi-spin mode executes the same logic repeatedly in a loop, but without UI animation to improve performance. It uses temporary balance and jackpot variables to simulate the full sequence safely before committing changes to st.session_state. At the end of execution, the application is refreshed using st.rerun() to reflect all updates.
After gameplay, the analytics section visualizes the system’s behavior over time. It includes a payout distribution chart, summary statistics such as mean, variance, and win rate, and a cumulative RTP curve that shows how the game converges over time.
Finally, the reset functionality restores the entire game state, clearing balance progression, jackpot accumulation, and all historical tracking data, effectively restarting the simulation from its initial configuration.
To make it easier to follow, below there is a quick video of the application where I simulate individual spins as well as multi-spin runs.
Video 1. Streamlit Tab Game
As we start slowly with only a few spins, there is a strong effect of luck on whether we make or lose money.
As soon as we start adding multispins, the RTP begins to converge toward a previously established configuration, driven by the fixed setup of weights and multipliers.
If we keep the game running for a sufficient number of spins, the RTP will stabilize below 1, let’s say around 0.96. This means that, over time, all money wagered is expected to be lost, with the house retaining the edge.
Of course, due to randomness, within a limited number of spins a player can be profitable and even beat the house. However, if they continue playing in an attempt to increase profits, they will eventually return to their starting balance.
As the risk of ruin starts to kick in, the player may begin to irrationally chase the big wins they experienced before — wins that are, and have always been, rare.
That is precisely how gambling works: randomness creates the illusion that a few lucky players can make a lot of money, but nothing is said about when a lucky player decides to stop. In reality, for every player who manages to “break the bank,” the house has already made far more money from the losses of everyone else.
In this game, we don’t even consider the commission, which is a fraction of each bet returned to the house. In other words, it doesn’t matter who wins or loses, as long as the house keeps making money.
In a way, all the money that enters may eventually leave the system, but the house always keeps the fees.
So, how does the house always win?
We intuitively demonstrated that no matter how lucky players are, if the RTP is configured to negative returns, the house will eventually take all the money from a user.
Now, let’s prove it mathematically by running a multi-player system that simulates the house perspective of the game.
We achieved that in tab_sim, as depicted below:
with tab_sim: st.write(f"Jackpot pool: ${st.session_state.jackpot:.2f}") st.subheader("Monte Carlo Simulation for Current Weights/Multipliers")
enable_change = st.checkbox("Change parameters during simulation")
col_change1, col_change2 = st.columns(2) with col_change1: change_spin = st.number_input( "Spin number to change", min_value=1, max_value=50000, value=1000, disabled=not enable_change )
with col_change2: new_rtp_target = st.number_input( "Target RTP after change", min_value=0.5, max_value=2.0, value=1.05, step=0.01, disabled=not enable_change )
st.checkbox("Create gif", key="create_gifs")
bet = st.number_input("Simulation Bet", 1, 500, 10, key="sim_bet") n_rounds = st.number_input("Rounds per simulation", 10, 50000, 2000) n_simulations = st.number_input("Number of simulations", 1, 50, 10)
if enable_change and not change_applied and spin_num >= change_spin and new_weights is not None: current_weights = np.array(new_weights) current_multipliers = dict(new_multipliers_dict) change_applied = True
grid = [result[i:i+3] for i in range(0, 9, 3)] positions = get_positions(grid)
jackpot_hit = any(line.count("jackpot") == 3 for line in positions) if jackpot_hit: sim_jackpot_count += 1 jackpot_spins.append(spin_num) jackpot_events.append((sim_idx + 1, spin_num))
plot_trajectory(all_runs, change_spin if enable_change and new_weights is not None else None)l_runs, change_spin if enable_change and new_weights is not None else None)
This section runs a Monte Carlo simulation of the game using the current weights and multipliers, allowing us to observe how the system behaves under repeated random outcomes.
The simulation is built around multiple configurable parameters. It starts by displaying the current jackpot pool and then allows the user to define whether the system should dynamically change parameters during execution.
If enabled, a target RTP and a spin threshold are used to trigger an optimization step that recalibrates weights and multipliers mid-simulation.
Each simulation independently initializes the player’s balance and repeatedly executes spins until either the balance is insufficient or the maximum number of rounds is reached.
During each spin, the bet contributes partially to the jackpot pool, while the remaining amount is used in the gameplay outcome, which is generated based on the current configuration of symbols, weights, and multipliers.
The system also tracks key financial and statistical metrics such as total bet volume (total_bet), total payouts (total_payout), jackpot occurrences, and balance evolution across all simulations. When jackpot conditions are met, the event is recorded and later aggregated to estimate frequency and distribution patterns.
At the end of the execution, all simulation trajectories are aligned and analyzed together. This allows us to compute summary statistics such as mean, minimum, maximum, and standard deviation of final balances, as well as empirical RTP derived from observed payouts versus total bets.
In addition, theoretical RTP values are computed both for the initial configuration and, if applicable, for the dynamically adjusted system. This enables a direct comparison between expected performance and simulated outcomes under stochastic conditions.
Finally, the results are visualized through balance trajectories over time, optionally marking the point at which parameter changes occur, providing a clear view of how system behavior evolves under both static and adaptive configurations.
If you run all those steps, Streamlit will render a Monte Carlo Simulation tab as the Video 2, below:
Video 2. Streamlit Tab Sim
Now consider the showed paramegers such as:
Any initial player bet is set to $10.
Each simulation runs 2,000 spins.
The total number of simulations is 10.
And you may come up with a possible simulation as Figure 3 below:
Figure 3. Multiplayer Simulation with a $10 Bet and 2,000 Spins
We can learn from that figure that, across the 10 simulated games, the balance tends to reach zero around 400 spins. However, some lucky runs, such as Sim 1, are able to temporarily recover losses through short winning streaks.
We can also observe the effect of the jackpot mechanism (Sim 5), which shifts a losing trajectory into a temporarily winning position, reinforcing the perception that a few players can win significantly while others lose.
Nevertheless, given enough time, even Sim 5’s final balance converges back toward zero, assuming the RTP remains unchanged throughout the process.
In general, the system recorded a total of 6,419 spins and 2 jackpot events, resulting in a jackpot frequency of approximately 1 in 3,209 spins.
Only 2 simulations experienced at least one jackpot occurrence, specifically Sim 5 and Sim 7, each with a single jackpot hit at spins 874 and 319 respectively.
The final balance distribution shows a mean final balance of $7.02, with values ranging from a minimum of $2.6 to a maximum of $9.8. The standard deviation is $2.4, indicating a moderate dispersion across simulation outcomes.
From a probabilistic standpoint, the initial RTP and all derived RTP measures remain consistent at approximately 0.8663, confirming that both the theoretical and simulated RTP converge closely.
The observed RTP from the simulation is 0.8653, which is extremely close to the expected value, reinforcing the stability of the model under repeated trials.
From this, we can conclude that the system behaves consistently under stochastic conditions, with empirical outcomes closely matching theoretical expectations.
However, this also highlights the structural asymmetry of the game: over time, the house retains a persistent advantage.
Finally, does the house always win?
Yes, it does!
But how?
To understand this, we need to look at tab_opt, which searches for the optimal configuration of weights and multipliers, ensuring that the system is tuned in a way that guarantees long-term profitability for the house.
if st.button("Optimize RTP Parameters"): with st.spinner("Optimizing parameters..."): w_opt, m_opt, rtp_achieved, history = optmize_RTP( target_rtp, n_symbols=len(SYMBOLS), weight_bounds=(min_weight, max_weight), multiplier_bounds=(min_multiplier, max_multiplier), n_restarts=20 )
if w_opt is not None: st.write("**Optimized Parameters**") st.write(f"**Achieved RTP:** {rtp_achieved:.4f}") st.write(f"**Target RTP:** {target_rtp:.4f}") st.write(f"**Error:** {abs(rtp_achieved - target_rtp):.4f}")
results_df = pd.DataFrame({ 'Symbol': SYMBOLS, 'Weight': [f'{x:.2f}' for x in w_opt], 'Multiplier': [f'{x:.2f}' for x in m_opt] }) st.dataframe(results_df, hide_index=True)
If a slot game operates legally under national regulations, the RTP must be transparent and clearly presented to players, since it is, after all, a game of chance.
However, many online platforms operate outside proper regulation, where the house can adjust the RTP dynamically. This can be used to offer higher returns at the beginning and, as the player becomes more engaged, shift the system toward a lower RTP by making high-paying symbols rarer or reducing multipliers.
This section, tab_opt, is responsible for actively tuning the game configuration in order to match a target RTP.
The user defines a desired target_rtp, along with constraints for weights and multipliers through min_weight, max_weight, min_multiplier, and max_multiplier. These bounds control the search space in which the optimization algorithm operates.
Once triggered, the system calls optmize_RTP, which attempts multiple configurations (n_restarts) to find a combination of weights and multipliers that produces an RTP as close as possible to the target. The result includes the optimized parameters (w_opt, m_opt), the achieved rtp_achieved, and a history of attempts.
If a valid solution is found, the system reports the achieved RTP, compares it to the target_rtp, and displays the absolute error. It also presents a structured view of each Symbol alongside its corresponding Weight and Multiplier.
The optimized parameters are then persisted into st.session_state.weights and st.session_state.multipliers, meaning all subsequent simulations and game executions will use this updated configuration.
Additionally, if optimization history is available, the process is visualized over time. Each attempt is plotted against its resulting RTP, highlighting the best approximation found. A reference line marks the target_rtp, along with a tolerance band, making it easy to assess convergence and stability.
If no valid configuration is found within the defined bounds, the system returns an error, indicating that the search space may need adjustment.
When rendering it in Streamlit, we come up with Video 3, below:
Video 3. Streamlit tab_opt
Now consider, an experiment where:
We optimize the initial parameters to achieve a positive RTP of 1.05.
Players are then allowed to play 500 spins under this initial configuration.
After that, the RTP is re-optimized to a decreasing return, for example 0.95.
Players then continue for another 500 spins under the new configuration.
The outcome is shown in Figure 4 below:
Press enter or click to view image in full size
Figure 4.Simulating a Game with a Changing RTP
What we observe is that, among a small group of players, a significant portion initially becomes profitable, benefiting from the early positive return environment. However, after the parameter shift at 500 spins, the system starts to gradually reverse these gains.
By the end of 1,000 spins, the distribution of outcomes changes drastically: some players experience full ruin, some return close to their initial balance of $1,000.00, and only a small fraction remains profitable.
Most importantly, the system naturally drifts toward a state where losses dominate over time. As the process continues, the expected equilibrium is that almost all players eventually lose their gains and return value back to the house.
Many days of struggle, a few days of glory.
Well, not for the house.
Final Thoughts
This article covered the basic concepts of gambling games, explaining how over time the player tends to lose more money than they win.
It aimed to contextualize the emotions and passions of players, who in moments of euphoria and wins tend to believe that the machine is fair and that luck favors the brave and the bold.
However, in the long run, uncertainty is structured in a way that returns less than what is wagered, and the player eventually reaches ruin.
Here we demonstrated that no matter how lucky players are they end up to give all their savings to the house.
This context raises the question of whether casino games should be permitted in modern society. They function as entertainment, yet mathematically they are structured toward long-term ruin, economically they transfer wealth from many to a few, and politically they can reinforce existing financial power structures.
These broader implications will be examined in future work on online betting.
Thanks for reading! If you enjoyed this article, feel free to give it a clap.
MICROGAMING. Game Provider RTP and Volatility Data. In: Top 5 Game Providers Behind Goldwin Casino’s Exciting Selection. bdvo.org, 2026. Available at: https://archive2021.bdvo.org.
NETENT. Game Provider Technical Documentation. In: Top 5 Game Providers Behind Goldwin Casino’s Exciting Selection. bdvo.org, 2026. Available at: https://archive2021.bdvo.org.
ECOGRA. Testing Standards and RNG Certification. In: eebguide.eu. All Slots New Game Genres To Try This Year, 2026. Available at: https://eebguide.eu.
Why I
don’t really care if web content is AI-generated
I’ve noticed people taking an interest in the “small net” or “small
web” because they’re fed up with the amount of material on the
mainstream web which is generated by large language models (LLMs) and
the like. They complain about “AI slop”, which I take to mean
poor-quality, low-effort, unoriginal content.
There is a lot of this kind of material on the mainstream
web; it’s hard to be sure exactly how much, because authorship is often
concealed. “Slop” is now starting to appear on the “small” web, too – so
it might not always be the refuge it once was. But that’s a matter for
another day.
I confess to a visceral distaste for AI-generated content, but I
can’t really explain why. I’ve noticed, thought, that that many other
people who find AI troubling also can’t explain why. When they try, the
most common complaints I hear are that
AI-generated content might be inaccurate, or
it’s “inauthentic”, or
it’s not clear that AI was used in the authoring.
Let’s consider these contentions, one by one.
Is AI-generated content
inaccurate?
Well, yes, it often is. But a lot of human-generated content is
inaccurate, too. Social media, in particular, carries a lot of
poorly-researched material that lacks nuance. Sometimes there is
deliberate, wanton misinformation. LLMs can produce false and misleading
information – but only because they’ve been trained on such material. So
far as I know, an LLM can’t (yet) act in bad faith – immorality requires
the special creativity of the human mind.
LLMs are actually quite good at aggregating and summarizing large
volumes of information. It’s true that they lack discernment; that is,
they’re unable to distinguish fine shades of meaning. But, frankly, many
people are not sufficiently analytical in their reading, either.
Critical assessment of information isn’t taught widely enough, from a
young enough age, but it should be.
In short, while there is a lot of “AI slop” around, there’s an awful
lot of “human slop”, too – along with human deliberate mischief-making
and sometimes outright wickedness.
Is AI-generated content
inauthentic?
I recently read on a web forum a moving account of a woman’s heroic –
and ultimately unsuccessful – struggle with cancer, written by her
husband of thirty years. Almost before my tears had dried, somebody else
had posted that the same text appeared under different names in at least
three different places, and was “AI slop”. People with a nose for that
kind of thing seem to be able to recognize the signs, even though I did
not.
What makes this sort of thing offensive? This AI-generated story was
true, in the sense that it was an aggregate of real human
experiences, as derived from the LLM’s training data. There wasn’t a
real, named human being undergoing the pain and loss but, sadly, there
are such people. If the story had been written directly by a
human, I doubt the writer would be somebody I knew personally, who could
confirm that all the details were true (even if I were heartless enough
to ask).
In short, the impact of this story on me, personally, would have been
the same, whoever or whatever wrote it. So why is there a problem if
it’s AI-generated?
Fundamentally, I think the issue is that we don’t like to be made to
sympathize with somebody who isn’t real. No, we do like that:
we like it in fiction. We just don’t like it when the story purports to
be factual. It takes emotional energy to empathise. Why it does, when
the writer is a complete stranger to us, is something I’m still
struggling to understand.
I struggle even more to understand why people claim AI-generated
content is unauthentic even when it’s fiction: fiction is
inauthentic by its very nature, whoever writes it. I doubt that Charles
Dickens ever had the experience of waiting in line to be guillotined,
but we don’t criticise him for writing as if he had, because A Tale
of Two Cities is a work of fiction. I’d argue that being able to
live another person’s experiences is a key skill for a novelist,
whatever those experiences are.
Of course, an LLM doesn’t have any experiences – it’s a
machine. But what an LLM can do is to aggregate the experiences of human
writers. The results of this process might be a bit bland, but they’re
no less authentic than any other work of fiction.
Does it matter
whether we know AI was used?
It certainly matters to many people. I suspect these people want to
know that AI was used because then they don’t have to read the content.
If you think it’s likely to be inaccurate or “inauthentic”, you’ll
certainly save time by skipping over it.
Some people won’t read AI-generated content as a matter of principle,
whether it might be useful or not. If you think AI is taking people’s
jobs, for example, I can see why you might eschew it completely. I’m
sure AI is taking people’s jobs. Database company Oracle
announced earlier this week that it was laying off 10% of its workforce,
citing “increased uptake of AI” as the reason. Whether AI will create a
compensating number of new job roles remains to be seen.
Whether I read the AI-generated summary of this morning’s news
stories, or skip it and search for something more human, I doubt it will
change the uptake of AI in the IT industry. Nevertheless, I can
understand why opponents of AI might not want to be complicit in this
hostile takeover.
So it seems to me that it’s just common courtesy for a publisher to
indicate how AI was involved, given that many people seem to want to
know.
But it doesn’t really matter much to me.
Why I don’t care
The complaints about AI in web-based publishing seem to me to have at
least some merit. So why do I find that I’m not particularly
worried?
For my part, I divide all on-line content into two classes:
material written by a known individual (or organization) that I trust,
and everything else, in a ratio of about 1:1000. In the “everything
else” category I find that I don’t really distinguish between human and
AI-generated content: everything falls to be assessed on its
own merits. Some AI-generated content is informative and entertaining,
but not much; but the same applies to human-generated content. I think
it’s a grave error to assume that human-generated content is a
priori more likely to be accurate than AI content: very few
individuals produce material that I trust without detailed scrutiny, and
it takes a long time for any writer to make it onto my trustworthy
list.
It seems plausible to me that the category of “known individuals I
trust” might one day encompass AI agents. If the AI consistently
produces information that turns out to be true, useful, or entertaining,
I can see how I might one day accept it as an “individual” for practical
purposes. That won’t happen easily, but I don’t easily trust human
writers, either.
We’re living at the end of the “Gutenberg parenthesis”. This was the
time period – short in terms of human history – during which information
was managed institutionally. Until the invention of the printing press,
information was largely transmitted orally, and held communally.
Printing allowed information to be centralized – exploited, some might
say – by large institutions that could afford printing. These
institutions had strong, financial incentives to print stuff that was
essentially accurate. In my youth, “I read it in a book” was
more-or-less a knock-down argument that some fact was true. Before we
realized the way the Internet was going, we carelessly extended the same
reasoning to web-based content, whose authors had no particular
incentive to share only truthful information.
The Internet has made information communal again, but we haven’t yet
(re-)learned the skills we need to handle this kind of information
properly. It’s no longer obvious which sources can be trusted, and we
don’t yet have robust methods to find out.
As a result, scepticism about all on-line information is
justified, whether it’s the product of the human mind or a machine.
In short, I’m not worried about AI-generated web content because we
need to learn to think about information in pre-Gutenberg terms with
or without AI.
Have you posted something in response to this page?
Feel free to send a webmention
to notify me, giving the URL of the blog or page that refers to
this one.