Eureka Phone: Duo
Exports and integrations
Server exports for your own resources, the service-message allowlist, and how a new phone design plugs into the core.
Everything is exported by the core, eureka_phone, never by a page resource. A page resource is client-only and cannot grant anything.
Server exports
| Export | Returns | Notes |
|---|---|---|
GetPhoneNumber(playerSource) |
string or nil |
The number of the active line of the phone the player has open, while that session passes the checks every request repeats (same character, phone still carried). Reads the in-memory session; cheap to call often. |
IsPhoneAvailable(playerSource) |
boolean |
Whether the player has such a phone open. |
SendServiceMessage(toNumber, body, options, callback) |
nothing | Sends body from the server's service line to toNumber. See below. |
GetMailAddress(playerSource) |
string or nil |
The player's in-game mail address (mail app). |
SendServiceMail(...) |
nothing | Sends mail from a configured sender (mail app). |
SetCryptoPrice(price, callback) |
nothing | Sets the fictional coin's price from a price feed resource (crypto app, duo_crypto_price_resources). |
-- Who is this player on the phone?
local number = exports.eureka_phone:GetPhoneNumber(source)
if not number then
return -- no phone open, or it was taken away
end
-- A message from your resource's service line. The callback always runs exactly once.
exports.eureka_phone:SendServiceMessage(number, 'Your vehicle is ready at Benny’s.', { reference = 'repair-4812' },
function(messageId, errorCode)
if not messageId then
print(('phone refused: %s'):format(errorCode)) -- NOT_ALLOWED, RATE_LIMITED, UNAVAILABLE, ...
end
end)How service messages work
- Allowlist. The core serves
SendServiceMessageonly to resources named induo_service_resources(for example"my_bank,my_garage"); callers never choose the sender. Setduo_service_numberto the number messages come from, and keep it induo_reserved_numbers. - Idempotent retries. Pass
options.reference: a retry with the same reference returns the first message instead of sending again. Keep it when the callback answersUNAVAILABLE(the core stopped mid-call) and retry after it is back. - Rate limit.
duo_rate_limit_per_minutealso caps how many service messages one resource sends to one number per minute (RATE_LIMITED). - Callback. Required; it runs on a later tick after the database answered, with
(messageId, nil)or(nil, errorCode). Error codes are the phone's own (NOT_READY,NOT_ALLOWED,INVALID_OPTIONS,RATE_LIMITED,UNAVAILABLE,INTERNAL).
Mail works the same way: list sender resources in duo_mail_senders as resource:name.
Island activities
Other resources can show live activities on the Island (the pill at the top of the phone) once they are listed in duo_island_resources. The contract is in the archive's apps/island/shared/contract.lua.
Price feeds
A resource listed in duo_crypto_price_resources may call SetCryptoPrice to move the coin's price; duo_crypto_price is the starting price and duo_crypto_history_days how much history players see.
How a phone design plugs in
A phone design is a page resource: its built page (web/dist), a config/client.lua with its items and keys, and a manifest that runs the core's bridge in it. It has no server script and lists no app contract; the bridge reads the app events from the core at load (exports.eureka_phone:netEvents()), so an app added to the core needs no change in any page.
fx_version 'cerulean'
game 'gta5'
name 'eureka_duo'
dependency 'eureka_phone'
client_scripts {
'@eureka_phone/config/shared.lua',
'@eureka_phone/shared/contracts.lua',
'@eureka_phone/shared/errors.lua',
'config/client.lua',
'@eureka_phone/client/bridge.lua',
}
ui_page 'web/dist/index.html'
ui_page_preload 'yes'
nui_callback_strict_mode 'true'
files { 'web/dist/index.html', 'web/dist/**/*' }What differs between designs, and must: the resource name (its NUI origin https://cfx-nui-<name>, its request IDs, its own key commands +<name>_<key>) and its items. What stays the same: the core's name in the manifest, the network events, the convars and the tables.
The core's router keeps one current page per player: the page of the item they opened last, else the page that was current, else the first page that started. The phone key shows that page. When the server opens a phone whose item another page lists, the first page hands over once and the other page shows and opens. See running both designs.
Last updated October 2, 2026