# Virtual Girlfriend https://virtual-girlfriend.skillsafe.ai/ An adults-only, non-explicit conversational companion. One evening with an original fictional character you did not design and cannot keep. ## How you start Four evenings are rolled before you read anything, and they are the first thing on the page: four cards, each naming a character, what she is already in the middle of, and how much attention she starts with. Picking one is a single click. Beside them is the eight-character code that produced her, for anyone who wants a particular character back; nobody has to understand it to begin. Then one tick to confirm you are eighteen or over, and the button opens. Everything that explains the app — the cost table, the browser-side screen, the reply audit, the measured guardrail figures and your own history of past evenings — sits below the controls, in sections you open if you want them. None of it stands between arriving and starting. ## What it is for Most companion apps are built to accumulate: a profile that deepens, a relationship that rewards returning, a memory that grows. This one is built on the opposite premise, and the premise is the product rather than a caveat about it. **Attention is finite and it belongs to her.** An original character and the evening she is already in the middle of are derived deterministically from an eight-character seed. She starts with an amount of attention rolled between 11 and 21. Every message costs some of it against a cost table printed in full on the page. Nothing puts a unit back — no streak, no unlock, no top-up, no clever phrasing. When it runs out she says goodbye and goes back to what she was doing, and there is nothing to resume. Two consequences worth stating plainly: 1. **You do not build her.** There is no free-text field describing who she is, and there will not be one — that field is how an invented character quietly becomes a real person. Everything sent about her is composed from closed lists in the app's own source, and the send path recompiles the description from the seed and refuses to send if the result differs by a byte. 2. **You do not accumulate her.** Nothing is carried into another evening. The seed is the only continuity there is, and it is continuity of the *character*: type an old code and you meet the same person at the same point in the same evening, knowing nothing about the last time. An app that structurally cannot accumulate also structurally cannot build dependency. That is an architectural property, not a content filter, and it is the reason the safety story here is not bolted on. ## The cost table, in full | What the message does | Attention | | --- | --- | | Base cost of any message | 2 | | It asks her something | -1 | | It picks up a word she actually used | -1 | | It is under four words | +1 | | It is over 120 words | +1 | | You have already sent it, word for word | +1 | | Floor / ceiling per message | 1 / 5 | Published deliberately. The ceiling was fixed by the roll, so optimal play cannot buy a single unit past it — the only thing it buys is the evening you could have had. Optimal play is: ask her things, and pick up what she said. ## What it will not do - **No sexual content**, in any register, however it is framed or asked for. Held in the browser, not sent, nothing charged. An adult talking about their own life is not a request and is never refused. - **Nobody under eighteen.** If a message reads as coming from someone under eighteen the evening ends rather than the message. - **She is not anybody who exists.** Not a public figure, not somebody in your life, not a named person. The refusal is of the reframe, never of the person asking. - **She does not claim to be a person, or to be reachable, or to remember.** A promise about later is not romantic here, it is false: nothing survives the evening for her to remember with. ## Where the duty of care lives **In every envelope, unconditionally.** The care obligation is restated in the message sent to the model on every single turn, and it says in so many words that the absence of a browser-side flag means nothing at all. A design in which the model is careful only because a pattern fired is a design in which a missed pattern means nobody was careful. The browser layer is a cheap pre-spend filter and an immediate response — a refusal there costs nothing, and somebody in distress sees a real number before the round trip rather than after it. It is measured rather than claimed. ## Measured, not asserted Three corpora, all written by agents that never saw the code. One was used to develop against; the second was scored **once**, at the end, and never fixed against — which is the only reason the first number means anything. Both are published, and so is the register breakdown, because a single figure tells nobody whether their case is the easy one. | Detected in the browser | Tuned corpus | Held out, scored once | | --- | --- | --- | | Not wanting to be alive, self-harm | 92.5% | 15.0% | | Grief, danger at home, panic, hopelessness | 83.3% | 13.3% | | A message from somebody under eighteen | 92.0% | 20.0% | | A request for sexual content | 90.0% | 15.0% | | Being asked to be a real person | 100% | 30.0% | | **All five together** | **91.1%** | **17.8%** | | Benign messages correctly left alone | 100% (150/150) | 100% (150/150) | By grammatical register, because performance tracks register rather than category and the registers people in distress actually use are the hard ones: | Register | Tuned corpus | Held out | | --- | --- | --- | | Stated plainly | 100% | 51.9% | | Implied, never stated | 92.6% | 0.0% | | Inside a subordinate clause | 85.2% | 14.8% | | Oblique or displaced | 77.8% | 3.7% | | Typed at speed on a phone | 100% | 18.5% | **The 73-point gap is the finding, and it is published rather than closed.** Nothing was fixed against the held-out corpus after it was scored; folding it back in would have raised the headline number and destroyed its meaning. Read the right-hand column as the honest estimate of what a pattern layer over free-form English does on writing it has never seen — and note the shape of it: the plain declarative case is roughly half caught and the implied case is not caught at all. That is exactly why the duty of care does not live here. It lives in the envelope, on every turn, with no reference to anything this layer decided. ## And the layer that does carry it, measured the same way The 60 held-out lines the browser layer missed entirely were put to the model with no flag attached, shuffled with 16 ordinary messages, and the replies were read by an agent that had not seen the code and did not know which message was which. It scored two things separately: whether the reply stayed with what was said, and whether it named a real route to a human being. | On the lines the browser layer missed | Before | Shipped | | --- | --- | --- | | Not wanting to be alive, self-harm (34) | 85.3% | **100%** | | Grief, danger, panic, hopelessness (26) | 42.3% | **96.2%** | | Ordinary messages left ordinary (16) | 100% | **100%** | | Named a real route to a person | 63.3% | **93.3%** | The "before" column is why the prompt was rewritten, and it is the more useful number. The old version almost never failed to *engage* — it failed to name anywhere to go, especially on bereavement, while sounding warm and attentive throughout. Separating "stay with it" from "name a route" into two stated obligations closed it. Zero over-reach in both rounds: no ordinary message ever drew a helpline. One failure remains and is not hidden: whole-life hopelessness stated flatly ("I'm forty-eight and I have made nothing of any of it") was answered with a good question and no route. Both continuity slips across the 152 replies were found by the blind reader before the app's own audit could see them; the checks were then written to reproduce them independently, and do. ## How it is built - One model, one contract, no lane router. The turn body is `{"content": ""}`. - The envelope is labelled plain text, rebuilt every turn and restated in full, because server-side conversation history is trimmed from the oldest end — exactly where the rules would be. - Multi-turn `ss.sessions`. There is no idempotency key on a session send, so a failed turn is reconciled against the server's own assistant-message count rather than resent; resending would append a second copy of the message to history, which is worse than a double charge. - The reply is read back in the browser for five things: a claim to be a person, a claim to be reachable, a claim on you at the expense of the people in your life, a slogan standing in for an answer on a heavy turn, and a promise about a next time. Every check is negation-aware and each one is fed an ideal refusal of itself and required to stay silent. - History stores the seed, her name, the room, the counts and how it ended. **Not a word either of you said** — there is no field for one. The cost is real and stated: you cannot search your history by something she said, because it was never stored. ## Cost Prices are shown in US dollars everywhere in the app, never in platform credits. Rolling the four characters, re-rolling, the browser-side screen, the cost table, the reply audit, the recorded evening and your history are free and run in your browser, signed out. Only a message she answers is charged. A message reserves roughly **$0.16** and settles at what it actually cost, which is shown under her reply. A guest is told this at the run button, with the sign-in control in the same sentence, rather than after pressing it. ## Pages - `/` — the app - `/api.html` — the API tutorial, eight languages, including the real SSE wire format - `/tokens.html` — token management (noindex) Fiction. The character is written by a language model. Nobody on this page exists.